@legendapp/spark 0.0.1-next.3 → 0.0.1-next.4

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 (83) hide show
  1. package/camera.ts +13 -0
  2. package/package.json +21 -11
  3. package/patches/camera/react-native-nitro-image.patch +320 -0
  4. package/patches/camera/react-native-nitro-modules.patch +50 -0
  5. package/patches/camera/react-native-vision-camera.patch +2662 -0
  6. package/patches/camera/upstream.json +22 -0
  7. package/patches/dependencies.json +8 -0
  8. package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
  9. package/postinstall.cjs +1 -0
  10. package/ui-swipe.ts +1 -0
  11. package/vendor/node_modules/@legendapp/spark-ai/package.json +3 -3
  12. package/vendor/node_modules/@legendapp/spark-audio/package.json +2 -2
  13. package/vendor/node_modules/@legendapp/spark-auth-session/package.json +3 -3
  14. package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +62 -11
  15. package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +2 -1
  16. package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +3 -2
  17. package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +6 -3
  18. package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +41 -26
  19. package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
  20. package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +1 -1
  21. package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +1 -1
  22. package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
  23. package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
  24. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
  25. package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
  26. package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +43 -7
  27. package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
  28. package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +3 -1
  29. package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
  30. package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +27 -22
  31. package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +2 -1
  32. package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +19 -19
  33. package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
  34. package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +1 -8
  35. package/vendor/node_modules/@legendapp/spark-cli/package.json +3 -3
  36. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +2 -2
  37. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +2 -2
  38. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +2 -2
  39. package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
  40. package/vendor/node_modules/@legendapp/spark-codex/package.json +3 -3
  41. package/vendor/node_modules/@legendapp/spark-commands/package.json +5 -5
  42. package/vendor/node_modules/@legendapp/spark-context-menu/package.json +2 -2
  43. package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +1 -1
  44. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +6 -4
  45. package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +2 -0
  46. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
  47. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
  48. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
  49. package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +5 -4
  50. package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
  51. package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
  52. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +2 -2
  53. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +39 -0
  54. package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +3 -3
  55. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +3 -1
  56. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +1 -1
  57. package/vendor/node_modules/@legendapp/spark-documents/package.json +7 -7
  58. package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +2 -2
  59. package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +2 -2
  60. package/vendor/node_modules/@legendapp/spark-file-system/package.json +2 -2
  61. package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +2 -2
  62. package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
  63. package/vendor/node_modules/@legendapp/spark-native-menu/package.json +2 -2
  64. package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
  65. package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +16 -11
  66. package/vendor/node_modules/@legendapp/spark-processes/package.json +2 -2
  67. package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
  68. package/vendor/node_modules/@legendapp/spark-settings/package.json +3 -3
  69. package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +4 -4
  70. package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
  71. package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
  72. package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
  73. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +263 -26
  74. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
  75. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
  76. package/vendor/node_modules/@legendapp/spark-ui/package.json +3 -2
  77. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +6 -0
  78. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +20 -11
  79. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
  80. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
  81. package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
  82. package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
  83. package/vendor/node_modules/@legendapp/spark-window-options/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-context-menu",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
44
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
45
45
  },
46
46
  "private": true
47
47
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-app",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -11,6 +11,7 @@ export type MenuItem =
11
11
  | { type: "separator" }
12
12
  | (MenuEntry & { type: "action"; shortcut?: string })
13
13
  | (MenuEntry & { type: "checkbox"; checked: boolean; shortcut?: string })
14
+ | (MenuEntry & { type: "radio"; checked: boolean; shortcut?: string })
14
15
  | (MenuEntry & { type: "submenu"; items: readonly MenuItem[] })
15
16
  | (Omit<MenuEntry, "label"> & { type: "role"; role: MenuRole; label?: string; shortcut?: string })
16
17
  | (MenuEntry & { type: "slider"; min: number; max: number; value: number; suffix?: string });
@@ -20,7 +21,7 @@ export type MenuItemTree<Leaf extends Exclude<MenuItem, { type: "submenu" }>> =
20
21
  /** Internal transport shape; never reexported from feature entry points. */
21
22
  export interface MenuWireItem {
22
23
  id?: string; title?: string; target?: MenuTarget; placement?: MenuPlacement; _sparkOwner?: string; _sparkIdentity?: string; enabled?: boolean; hidden?: boolean; checked?: boolean; separator?: boolean;
23
- shortcut?: Accelerator; items?: MenuWireItem[]; role?: MenuRole;
24
+ shortcut?: Accelerator; items?: MenuWireItem[]; role?: MenuRole; radio?: boolean;
24
25
  systemImageName?: string; imagePath?: string; slider?: { min: number; max: number; value: number; suffix?: string };
25
26
  }
26
27
  export interface MenuSupport { types: readonly MenuItem["type"][]; shortcuts?: boolean; icons?: readonly MenuIcon["type"][]; targeting?: boolean }
@@ -43,7 +44,7 @@ export function menuItems(input: readonly MenuItem[], support: MenuSupport, plat
43
44
  if (typeof item.id !== "string" || !item.id.length || item.id.length > 200 || item.id.includes("\0") || ids.has(item.id)) throw new SparkError("E_INVALID_ARGUMENT", "Menu item ids must be nonempty and unique");
44
45
  ids.add(item.id);
45
46
  const common = ["type", "id", "label", "disabled", "hidden", "icon", "target", "placement"];
46
- keys(item, [...common, ...(item.type === "submenu" ? ["items"] : item.type === "slider" ? ["min", "max", "value", "suffix"] : item.type === "role" ? ["role", "shortcut"] : item.type === "checkbox" ? ["checked", "shortcut"] : ["shortcut"])]);
47
+ keys(item, [...common, ...(item.type === "submenu" ? ["items"] : item.type === "slider" ? ["min", "max", "value", "suffix"] : item.type === "role" ? ["role", "shortcut"] : item.type === "checkbox" || item.type === "radio" ? ["checked", "shortcut"] : ["shortcut"])]);
47
48
  if ((item.type !== "role" || item.label !== undefined) && (typeof item.label !== "string" || !item.label.trim())) throw new SparkError("E_INVALID_ARGUMENT", "Menu items need a label");
48
49
  if ([item.disabled, item.hidden].some(value => value !== undefined && typeof value !== "boolean")) throw new SparkError("E_INVALID_ARGUMENT", "Menu flags must be boolean");
49
50
  const result: MenuWireItem = { id: item.id, title: item.label, enabled: !item.disabled };
@@ -57,9 +58,10 @@ export function menuItems(input: readonly MenuItem[], support: MenuSupport, plat
57
58
  result.placement = item.placement.before !== undefined ? { before: menuTarget(item.placement.before) } : { after: menuTarget(item.placement.after!) };
58
59
  }
59
60
  }
60
- if (item.type === "checkbox") {
61
- if (typeof item.checked !== "boolean") throw new SparkError("E_INVALID_ARGUMENT", "Checkbox menus require checked state");
61
+ if (item.type === "checkbox" || item.type === "radio") {
62
+ if (typeof item.checked !== "boolean") throw new SparkError("E_INVALID_ARGUMENT", "Checkbox and radio menus require checked state");
62
63
  result.checked = item.checked;
64
+ if (item.type === "radio") result.radio = true;
63
65
  }
64
66
  if (item.type === "submenu") result.items = visit(item.items, depth + 1);
65
67
  if (item.type === "role") {
@@ -69,6 +69,7 @@ function developmentConfig(value, target) {
69
69
  return result;
70
70
  }
71
71
  function expoConfig(root) {
72
+ require("./native-patches.cjs").prepareNativePatches(root);
72
73
  if (process.env.SPARK_DEV_SESSION === "1") {
73
74
  return developmentConfig(JSON.parse(fs.readFileSync(path.join(root, filename), "utf8")), process.env.SPARK_PLATFORM);
74
75
  }
@@ -137,6 +138,7 @@ function merge(base, override) {
137
138
  // Expo tools read this generated transport file. All framework consumers resolve
138
139
  // the canonical source; the generated file is never edited by SDK commands.
139
140
  function prepareConfig(root) {
141
+ require("./native-patches.cjs").prepareNativePatches(root);
140
142
  if (isExpoProject(root)) return readConfig(root);
141
143
  if (isUniversal(root)) {
142
144
  const file = path.join(root, "app.config.js");
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  // Only an explicitly selected desktop command applies the Spark overlay.
6
6
  function withSparkExpo(original, root) {
7
7
  return context => {
8
+ require('./native-patches.cjs').prepareNativePatches(root);
8
9
  const base = typeof original === 'function' ? original(context) : original;
9
10
  const target = process.env.SPARK_PLATFORM;
10
11
  const development = process.env.SPARK_DEV_SESSION === '1';
@@ -0,0 +1,106 @@
1
+ const { createHash } = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const { createRequire } = require("node:module");
5
+
6
+ const cameraPackages = ["react-native-nitro-image", "react-native-vision-camera"];
7
+
8
+ function prepareNativePatches(root) {
9
+ const manifestFile = path.join(root, "package.json");
10
+ if (!fs.existsSync(manifestFile)) return {};
11
+ const app = JSON.parse(fs.readFileSync(manifestFile, "utf8"));
12
+ if (app.name !== "@legendapp/spark" && !app.dependencies?.["@legendapp/spark"] && !app.devDependencies?.["@legendapp/spark"]) return {};
13
+ const appRequire = createRequire(manifestFile);
14
+ let sparkFile;
15
+ try { sparkFile = appRequire.resolve("@legendapp/spark/package.json"); }
16
+ catch (error) { if (error.code === "MODULE_NOT_FOUND") return {}; throw error; }
17
+ const spark = JSON.parse(fs.readFileSync(sparkFile, "utf8"));
18
+ const directory = path.dirname(sparkFile);
19
+ const patchesRoot = spark.spark?.bundledModuleRoot
20
+ ? path.join(directory, "patches") : path.resolve(__dirname, "../../patches");
21
+ const pins = {};
22
+ if (cameraPackages.some(name => spark.dependencies?.[name])) {
23
+ const cameraPins = JSON.parse(fs.readFileSync(path.join(patchesRoot, "camera/upstream.json"), "utf8"));
24
+ for (const name of ["react-native-nitro-modules", ...cameraPackages]) {
25
+ pins[name] = { ...cameraPins[name], patch: `camera/${name}.patch`,
26
+ requires: name === "react-native-vision-camera" ? ["react-native-nitro-modules", "react-native-nitro-image"] : ["react-native-nitro-modules"] };
27
+ }
28
+ }
29
+ const integrations = path.join(patchesRoot, "dependencies.json");
30
+ if (fs.existsSync(integrations)) Object.assign(pins, JSON.parse(fs.readFileSync(integrations, "utf8")));
31
+ const active = Object.keys(pins).filter(name => spark.dependencies?.[name]);
32
+ if (!active.length) return {};
33
+ const { parsePatch, applyPatch, reversePatch } = require("diff");
34
+ const queue = [appRequire, createRequire(sparkFile)];
35
+ const visited = new Set();
36
+ const packages = {};
37
+ for (const req of queue) {
38
+ for (const name of active) {
39
+ let file;
40
+ try { file = req.resolve(`${name}/package.json`); }
41
+ catch (error) {
42
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
43
+ if (req === appRequire) continue;
44
+ throw new Error(`Spark needs ${name}@${pins[name].version}. Finish installing dependencies before native preparation.`);
45
+ }
46
+ if (!fs.existsSync(file)) throw new Error(`Spark needs ${name}@${pins[name].version}. Finish installing dependencies before native preparation.`);
47
+ const packageRoot = fs.realpathSync(path.dirname(file));
48
+ if (visited.has(packageRoot)) continue;
49
+ visited.add(packageRoot);
50
+ packages[name] = { root: packageRoot };
51
+ queue.push(createRequire(file));
52
+ const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
53
+ const pin = pins[name];
54
+ if (manifest.version !== pin.version) throw new Error(`Spark needs ${name}@${pin.version}; found ${manifest.version}. Use the SDK's pinned dependency version.`);
55
+ const bytes = fs.readFileSync(path.join(patchesRoot, pin.patch));
56
+ const writes = [];
57
+ for (const patch of parsePatch(bytes.toString("utf8"))) {
58
+ const relative = patch.newFileName.replace(/^b\//, "");
59
+ const target = path.resolve(packageRoot, relative);
60
+ if (!target.startsWith(packageRoot + path.sep) || patch.newFileName === "/dev/null") throw new Error(`Invalid native patch target: ${relative}`);
61
+ let current = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : "";
62
+ if (patch.oldFileName === "/dev/null") {
63
+ const next = applyPatch("", patch);
64
+ if (current === next) continue;
65
+ if (current !== "") throw new Error(`Cannot apply Spark's ${name} patch to ${relative}. Reinstall dependencies before retrying.`);
66
+ writes.push([target, next]);
67
+ continue;
68
+ }
69
+ if (relative === "package.json" && manifest.spark?.dependencyPatch) {
70
+ const original = JSON.parse(current);
71
+ delete original.spark.dependencyPatch;
72
+ delete original.spark.sdk;
73
+ delete original.spark.requires;
74
+ if (!Object.keys(original.spark).length) delete original.spark;
75
+ current = JSON.stringify(original, null, 2) + "\n";
76
+ }
77
+ if (applyPatch(current, reversePatch(patch)) !== false) continue;
78
+ const next = applyPatch(current, patch);
79
+ if (next === false) throw new Error(`Cannot apply Spark's ${name} patch to ${relative}. Reinstall dependencies before retrying.`);
80
+ writes.push([target, next]);
81
+ }
82
+ // Verify every file before writing; rename rather than mutate package-manager cache hardlinks.
83
+ for (const [target, next] of writes) {
84
+ fs.mkdirSync(path.dirname(target), { recursive: true });
85
+ const temporary = `${target}.spark-${process.pid}.tmp`;
86
+ fs.writeFileSync(temporary, next); fs.renameSync(temporary, target);
87
+ }
88
+ const patched = JSON.parse(fs.readFileSync(file, "utf8"));
89
+ patched.spark ??= {};
90
+ if (name !== "react-native-nitro-modules") {
91
+ patched.spark.sdk = true;
92
+ patched.spark.requires = pin.requires ?? [];
93
+ }
94
+ patched.spark.dependencyPatch = { schema: 1, version: pin.version,
95
+ patchHash: createHash("sha256").update(bytes).digest("hex"), upstreamIntegrity: pin.integrity };
96
+ const serialized = JSON.stringify(patched, null, 2) + "\n";
97
+ if (fs.readFileSync(file, "utf8") !== serialized) {
98
+ const temporary = `${file}.spark-${process.pid}.tmp`;
99
+ fs.writeFileSync(temporary, serialized); fs.renameSync(temporary, file);
100
+ }
101
+ }
102
+ }
103
+ return packages;
104
+ }
105
+
106
+ module.exports = { prepareNativePatches };
@@ -0,0 +1 @@
1
+ export function prepareNativePatches(root: string): Record<string, { root: string }>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-config",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "app.plugin.cjs",
6
6
  "files": [
@@ -11,10 +11,11 @@
11
11
  ],
12
12
  "dependencies": {
13
13
  "expo-desktop-config-plugins": "1.2.0",
14
- "@legendapp/spark-desktop-host": "0.0.1-next.3",
15
- "@legendapp/spark-window-options": "0.0.1-next.3",
14
+ "@legendapp/spark-desktop-host": "0.0.1-next.4",
15
+ "@legendapp/spark-window-options": "0.0.1-next.4",
16
16
  "@expo/config-plugins": "54.0.5",
17
- "@expo/config-types": "54.0.10"
17
+ "@expo/config-types": "54.0.10",
18
+ "diff": "8.0.2"
18
19
  },
19
20
  "private": true
20
21
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-host",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "AppDelegate.mm",
7
7
  "windows"
8
8
  ],
9
9
  "dependencies": {
10
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
10
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
11
11
  },
12
12
  "private": true
13
13
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-links",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -42,7 +42,7 @@
42
42
  ]
43
43
  },
44
44
  "dependencies": {
45
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
45
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "expo-linking": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-shortcuts",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -40,7 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
43
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
44
44
  },
45
45
  "private": true
46
46
  }
@@ -1219,7 +1219,37 @@ RCT_EXPORT_MODULE(NativeWindowManager)
1219
1219
  NSMutableDictionary *config = [item mutableCopy]; config[@"instanceId"] = objc_getAssociatedObject(window, NSSelectorFromString(@"sparkWindowInstanceId")) ?: @""; [configs addObject:config];
1220
1220
  }
1221
1221
  }
1222
+ NSArray<NSDictionary *> *previousConfigs = self.toolbarItemConfigs[toolbar.identifier];
1223
+ BOOL menuUpdate = previousConfigs != nil && previousConfigs.count == configs.count;
1224
+ for (NSUInteger index = 0; menuUpdate && index < configs.count; index++) {
1225
+ NSMutableDictionary *previous = [previousConfigs[index] mutableCopy];
1226
+ NSMutableDictionary *next = [configs[index] mutableCopy];
1227
+ if ([next[@"type"] isEqual:@"menuButton"]) {
1228
+ [previous removeObjectForKey:@"menuItems"];
1229
+ [next removeObjectForKey:@"menuItems"];
1230
+ }
1231
+ menuUpdate = [previous isEqualToDictionary:next];
1232
+ }
1222
1233
  self.toolbarItemConfigs[toolbar.identifier] = configs;
1234
+ if (menuUpdate) {
1235
+ for (NSDictionary *config in configs) {
1236
+ if (![config[@"type"] isEqual:@"menuButton"]) continue;
1237
+ NSString *itemId = config[@"id"];
1238
+ if (![itemId isKindOfClass:NSString.class] || itemId.length == 0) { menuUpdate = NO; break; }
1239
+ NSString *itemIdentifier = [@"legend.toolbar." stringByAppendingString:itemId];
1240
+ NSToolbarItem *item = nil;
1241
+ for (NSToolbarItem *candidate in toolbar.items) {
1242
+ if ([candidate.itemIdentifier isEqual:itemIdentifier]) { item = candidate; break; }
1243
+ }
1244
+ id control = item.view ?: item;
1245
+ NSDictionary *metadata = control ? objc_getAssociatedObject(control, &LegendToolbarControlMetadataKey) : nil;
1246
+ if (!metadata) { menuUpdate = NO; break; }
1247
+ NSMutableDictionary *nextMetadata = [metadata mutableCopy];
1248
+ nextMetadata[@"menuItems"] = config[@"menuItems"] ?: @[];
1249
+ objc_setAssociatedObject(control, &LegendToolbarControlMetadataKey, nextMetadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1250
+ }
1251
+ if (menuUpdate) return;
1252
+ }
1223
1253
 
1224
1254
  while (toolbar.items.count > 0) {
1225
1255
  [toolbar removeItemAtIndex:toolbar.items.count - 1];
@@ -2416,6 +2446,15 @@ willBeInsertedIntoToolbar:(BOOL)flag
2416
2446
  resolve([self failureJson:@"Toolbar item does not support text"]);
2417
2447
  return;
2418
2448
  }
2449
+ NSMutableArray *configs = [self.toolbarItemConfigs[window.toolbar.identifier] mutableCopy];
2450
+ for (NSUInteger index = 0; index < configs.count; index++) {
2451
+ if (![[self toolbarItemIdentifierForConfig:configs[index]] isEqualToString:toolbarIdentifier]) continue;
2452
+ NSMutableDictionary *config = [configs[index] mutableCopy];
2453
+ config[[toolbarItem.view isKindOfClass:NSTextField.class] ? @"text" : @"label"] = text ?: @"";
2454
+ configs[index] = config;
2455
+ break;
2456
+ }
2457
+ self.toolbarItemConfigs[window.toolbar.identifier] = configs;
2419
2458
  resolve([self successJson]);
2420
2459
  });
2421
2460
  #else
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-desktop-windows",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -40,8 +40,8 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@legendapp/spark-desktop-app": "0.0.1-next.3",
44
- "@legendapp/spark-window-options": "0.0.1-next.3",
43
+ "@legendapp/spark-desktop-app": "0.0.1-next.4",
44
+ "@legendapp/spark-window-options": "0.0.1-next.4",
45
45
  "@legendapp/state": "^3.0.0-beta.42"
46
46
  },
47
47
  "private": true
@@ -54,7 +54,9 @@ export async function getCursorPoint(): Promise<CursorPoint> { return windowCall
54
54
  export async function setWindowOptions(id: string, options: WindowUpdateOptions): Promise<void> {
55
55
  windowId(id); validateOptions(options, Platform.OS, false);
56
56
  const snapshot = JSON.parse(JSON.stringify(options)) as WindowUpdateOptions;
57
- await windowCommand("options", { id, options: snapshot });
57
+ if (!snapshot.macos || Object.keys(snapshot).some(key => key !== "macos")) {
58
+ await windowCommand("options", { id, options: snapshot });
59
+ }
58
60
  if (snapshot.macos) await updateMacOSWindow(id, snapshot.macos);
59
61
  }
60
62
  /** Portable frame change. Animated moves are macOS-only: pass `macos.durationMs` and the call rejects on Windows rather than ignoring the request. */
@@ -38,7 +38,7 @@ export function macosOptions(options: MacOSWindowOptions | undefined): Record<st
38
38
  if (options.toolbar.items) style.toolbarItems = options.toolbar.items.map(item => {
39
39
  const { type, ...common } = item;
40
40
  if (type === "segmented") { const value = item as Extract<typeof item, { type: "segmented" }>; return { ...common, type, selectedValue: value.value, segments: value.segments.map(({ symbol, ...segment }) => ({ ...segment, systemImageName: symbol })) }; }
41
- if (type === "menu") { const value = item as Extract<typeof item, { type: "menu" }>; return { ...common, type: "menuButton", enabled: !value.disabled, systemImageName: value.symbol, menuItems: menuItems(value.items, { types: ["action", "checkbox", "separator", "slider"], icons: ["symbol"] }, "macos") }; }
41
+ if (type === "menu") { const { items, ...value } = item as Extract<typeof item, { type: "menu" }>; return { ...value, type: "menuButton", enabled: !value.disabled, systemImageName: value.symbol, menuItems: menuItems(items, { types: ["action", "checkbox", "separator", "slider"], icons: ["symbol"] }, "macos") }; }
42
42
  return { ...common, type, ...("disabled" in item ? { enabled: !item.disabled } : {}), ...("symbol" in item ? { systemImageName: item.symbol } : {}) };
43
43
  });
44
44
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-documents",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "main": "src/index.tsx",
@@ -8,12 +8,12 @@
8
8
  "src"
9
9
  ],
10
10
  "dependencies": {
11
- "@legendapp/spark-file-dialog": "0.0.1-next.3",
12
- "@legendapp/spark-file-system": "0.0.1-next.3",
13
- "@legendapp/spark-native-menu": "0.0.1-next.3",
14
- "@legendapp/spark-desktop-app": "0.0.1-next.3",
15
- "@legendapp/spark-desktop-windows": "0.0.1-next.3",
16
- "@legendapp/spark-desktop-links": "0.0.1-next.3"
11
+ "@legendapp/spark-file-dialog": "0.0.1-next.4",
12
+ "@legendapp/spark-file-system": "0.0.1-next.4",
13
+ "@legendapp/spark-native-menu": "0.0.1-next.4",
14
+ "@legendapp/spark-desktop-app": "0.0.1-next.4",
15
+ "@legendapp/spark-desktop-windows": "0.0.1-next.4",
16
+ "@legendapp/spark-desktop-links": "0.0.1-next.4"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "react": "19.1.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-drag-drop",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.tsx",
6
6
  "react-native": "src/index.tsx",
@@ -39,7 +39,7 @@
39
39
  ]
40
40
  },
41
41
  "dependencies": {
42
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
42
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
43
43
  },
44
44
  "private": true
45
45
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-file-dialog",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "main": "src/index",
5
5
  "react-native": "src/index",
6
6
  "codegenConfig": {
@@ -45,6 +45,6 @@
45
45
  },
46
46
  "private": true,
47
47
  "dependencies": {
48
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
48
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
49
49
  }
50
50
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-file-system",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -43,7 +43,7 @@
43
43
  ]
44
44
  },
45
45
  "dependencies": {
46
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
46
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
47
47
  },
48
48
  "private": true
49
49
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-global-shortcuts",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
44
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
45
45
  },
46
46
  "private": true
47
47
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-message-dialog",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -40,7 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
43
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
44
44
  },
45
45
  "private": true
46
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-native-menu",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "main": "src/index",
5
5
  "react-native": "src/index",
6
6
  "codegenConfig": {
@@ -41,6 +41,6 @@
41
41
  },
42
42
  "private": true,
43
43
  "dependencies": {
44
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
44
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
45
45
  }
46
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-notifications",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -40,7 +40,7 @@
40
40
  ]
41
41
  },
42
42
  "dependencies": {
43
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
43
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
44
44
  },
45
45
  "private": true
46
46
  }
@@ -108,22 +108,27 @@ struct NotificationsState : std::enable_shared_from_this<NotificationsState> {
108
108
  auto audio = xml.CreateElement(L"audio");
109
109
  auto tone = std::wstring(L"ms-winsoundevent:Notification.");
110
110
  if (sound == L"mail") tone += L"Mail"; else if (sound == L"message") tone += L"IM"; else if (sound == L"reminder") tone += L"Reminder";
111
- else if (sound == L"call") { tone += L"Looping.Call"; audio.SetAttribute(L"duration", L"long"); }
111
+ else if (sound == L"call") { tone += L"Looping.Call"; audio.SetAttribute(L"loop", L"true"); xml.DocumentElement().SetAttribute(L"duration", L"long"); }
112
112
  else tone += L"Default";
113
113
  audio.SetAttribute(L"src", tone.c_str()); xml.DocumentElement().AppendChild(audio);
114
114
  }
115
115
  // Action buttons activate through the same CustomActivator as the toast body,
116
116
  // carrying their own action id in the activation arguments.
117
- for (auto const &entry : args.GetNamedArray(L"actions", Json::JsonArray())) {
118
- auto action = entry.GetObject();
119
- auto button = xml.CreateElement(L"action");
120
- // JsonObject is a reference type; parse a copy so each action carries its
121
- // own action id without mutating the body/dismiss payload.
122
- auto arguments = Json::JsonObject::Parse(response.Stringify());
123
- arguments.SetNamedValue(L"action", Json::JsonValue::CreateStringValue(action.GetNamedString(L"id")));
124
- button.SetAttribute(L"content", action.GetNamedString(L"label").c_str());
125
- button.SetAttribute(L"arguments", arguments.Stringify().c_str());
126
- xml.DocumentElement().AppendChild(button);
117
+ auto entries = args.GetNamedArray(L"actions", Json::JsonArray());
118
+ if (entries.Size() > 0) {
119
+ auto actions = xml.CreateElement(L"actions");
120
+ xml.DocumentElement().AppendChild(actions);
121
+ for (auto const &entry : entries) {
122
+ auto action = entry.GetObject();
123
+ auto button = xml.CreateElement(L"action");
124
+ // JsonObject is a reference type; parse a copy so each action carries its
125
+ // own action id without mutating the body/dismiss payload.
126
+ auto arguments = Json::JsonObject::Parse(response.Stringify());
127
+ arguments.SetNamedValue(L"action", Json::JsonValue::CreateStringValue(action.GetNamedString(L"id")));
128
+ button.SetAttribute(L"content", action.GetNamedString(L"label").c_str());
129
+ button.SetAttribute(L"arguments", arguments.Stringify().c_str());
130
+ actions.AppendChild(button);
131
+ }
127
132
  }
128
133
  auto tag = Hash(id); RemovePending(tag); Forget(tag);
129
134
  if (delay > 0) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-processes",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
44
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
45
45
  },
46
46
  "private": true
47
47
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-secure-storage",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
44
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "expo-secure-store": {
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@legendapp/spark-settings",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index.ts",
6
6
  "files": [
7
7
  "src"
8
8
  ],
9
9
  "dependencies": {
10
- "@legendapp/spark-file-system": "0.0.1-next.3",
10
+ "@legendapp/spark-file-system": "0.0.1-next.4",
11
11
  "@legendapp/state": "^3.0.0-beta.42",
12
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
12
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
13
13
  },
14
14
  "private": true,
15
15
  "spark": {