@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
@@ -0,0 +1,422 @@
1
+ #import "RNSwipeActions.h"
2
+ #import <QuartzCore/QuartzCore.h>
3
+ #import <react/renderer/components/RNSparkUISpec/ComponentDescriptors.h>
4
+ #import <react/renderer/components/RNSparkUISpec/EventEmitters.h>
5
+ #import <react/renderer/components/RNSparkUISpec/Props.h>
6
+ using namespace facebook::react;
7
+
8
+ static const CGFloat kButtonWidth = 76; // one revealed action when the row rests open
9
+ static const CGFloat kAxisDistance = 5; // travel before a gesture commits to an axis
10
+ static const CGFloat kRubberLimit = 36; // how far a side without actions gives
11
+
12
+ typedef NS_ENUM(NSInteger, RNSwipeGesture) { RNSwipeGestureIdle, RNSwipeGestureUndecided, RNSwipeGestureTracking, RNSwipeGesturePassing };
13
+
14
+ static NSColor *RNSwipeColor(NSString *hex)
15
+ {
16
+ unsigned long long raw = 0;
17
+ [[NSScanner scannerWithString:[hex substringFromIndex:1]] scanHexLongLong:&raw];
18
+ if (hex.length == 9) return [NSColor colorWithSRGBRed:((raw >> 24) & 0xff) / 255.0 green:((raw >> 16) & 0xff) / 255.0 blue:((raw >> 8) & 0xff) / 255.0 alpha:(raw & 0xff) / 255.0];
19
+ return [NSColor colorWithSRGBRed:((raw >> 16) & 0xff) / 255.0 green:((raw >> 8) & 0xff) / 255.0 blue:(raw & 0xff) / 255.0 alpha:1];
20
+ }
21
+
22
+ static void RNSwipeAnimate(NSTimeInterval duration, void (^changes)(void), void (^_Nullable completion)(void))
23
+ {
24
+ BOOL reduce = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceMotion;
25
+ [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
26
+ context.duration = reduce ? MIN(duration, 0.12) : duration;
27
+ context.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.2 :0.9 :0.25 :1];
28
+ context.allowsImplicitAnimation = YES;
29
+ changes();
30
+ } completionHandler:completion];
31
+ }
32
+
33
+ // React child frames use top-left coordinates, even inside private AppKit containers.
34
+ @interface RNSwipeContainerView : NSView @end
35
+ @implementation RNSwipeContainerView
36
+ - (BOOL)isFlipped { return YES; }
37
+ @end
38
+
39
+ @class RNSwipeActions;
40
+
41
+ @interface RNSwipeActionButton : NSView
42
+ @property (nonatomic, readonly) NSString *actionId;
43
+ @property (nonatomic, readonly) BOOL dismisses;
44
+ /** +1 on the leading side, -1 trailing; the outermost button tracks the content edge once armed. */
45
+ @property (nonatomic) NSInteger edge;
46
+ @property (nonatomic) BOOL followsContent;
47
+ @property (nonatomic, weak) RNSwipeActions *owner;
48
+ @end
49
+
50
+ @interface RNSwipeActions ()
51
+ - (void)performActionForButton:(RNSwipeActionButton *)button;
52
+ @end
53
+
54
+ @implementation RNSwipeActionButton {
55
+ NSImageView *_icon;
56
+ NSTextField *_label;
57
+ }
58
+ - (instancetype)initWithAction:(NSDictionary *)action
59
+ {
60
+ if ((self = [super initWithFrame:NSZeroRect])) {
61
+ _actionId = [action[@"id"] copy];
62
+ _dismisses = [action[@"dismisses"] boolValue];
63
+ self.wantsLayer = YES;
64
+ self.layer.backgroundColor = RNSwipeColor(action[@"color"]).CGColor;
65
+ NSImageSymbolConfiguration *config = [NSImageSymbolConfiguration configurationWithPointSize:17 weight:NSFontWeightSemibold];
66
+ _icon = [NSImageView imageViewWithImage:[[NSImage imageWithSystemSymbolName:action[@"symbol"] accessibilityDescription:nil] imageWithSymbolConfiguration:config] ?: [NSImage new]];
67
+ _icon.contentTintColor = NSColor.whiteColor;
68
+ _label = [NSTextField labelWithString:action[@"title"]];
69
+ _label.font = [NSFont systemFontOfSize:11 weight:NSFontWeightSemibold];
70
+ _label.textColor = NSColor.whiteColor;
71
+ _label.alignment = NSTextAlignmentCenter;
72
+ _label.lineBreakMode = NSLineBreakByTruncatingTail;
73
+ [self addSubview:_icon];
74
+ [self addSubview:_label];
75
+ self.accessibilityElement = YES;
76
+ self.accessibilityRole = NSAccessibilityButtonRole;
77
+ self.accessibilityLabel = action[@"title"];
78
+ }
79
+ return self;
80
+ }
81
+ - (BOOL)isFlipped { return YES; }
82
+ - (BOOL)acceptsFirstMouse:(NSEvent *)event { return YES; }
83
+ // Non-opaque views drag the window by default, which would swallow the click.
84
+ - (BOOL)mouseDownCanMoveWindow { return NO; }
85
+ - (void)layout
86
+ {
87
+ [super layout];
88
+ NSRect bounds = self.bounds;
89
+ CGFloat slot = MIN(bounds.size.width, kButtonWidth);
90
+ CGFloat center = !_followsContent ? NSMidX(bounds) : _edge > 0 ? bounds.size.width - slot / 2 : slot / 2;
91
+ CGFloat iconSize = 22, labelHeight = 15, top = floor((bounds.size.height - iconSize - 3 - labelHeight) / 2);
92
+ _icon.frame = NSMakeRect(round(center - iconSize / 2), top, iconSize, iconSize);
93
+ _label.frame = NSMakeRect(round(center - kButtonWidth / 2) + 4, top + iconSize + 3, kButtonWidth - 8, labelHeight);
94
+ // Contents fade in as the button gains room, so a sliver never shows clipped glyphs.
95
+ CGFloat alpha = MAX(0, MIN(1, (bounds.size.width - 20) / 36));
96
+ _icon.alphaValue = alpha;
97
+ _label.alphaValue = alpha;
98
+ }
99
+ - (void)mouseDown:(NSEvent *)event {}
100
+ - (void)mouseUp:(NSEvent *)event
101
+ {
102
+ if (NSPointInRect([self convertPoint:event.locationInWindow fromView:nil], self.bounds)) [_owner performActionForButton:self];
103
+ }
104
+ - (BOOL)accessibilityPerformPress { [_owner performActionForButton:self]; return YES; }
105
+ @end
106
+
107
+ static __weak RNSwipeActions *RNSwipeOpenRow;
108
+
109
+ @implementation RNSwipeActions {
110
+ NSView *_clipView;
111
+ NSView *_actionsView;
112
+ NSView *_contentView;
113
+ NSArray<RNSwipeActionButton *> *_leading;
114
+ NSArray<RNSwipeActionButton *> *_trailing;
115
+ std::string _leadingJson;
116
+ std::string _trailingJson;
117
+ NSMutableArray<NSEvent *> *_pending;
118
+ RNSwipeGesture _gesture;
119
+ CGFloat _pendingX;
120
+ CGFloat _pendingY;
121
+ CGFloat _offset;
122
+ /** The button filling the revealed strip: the outermost once armed, or a tapped dismissing one. */
123
+ RNSwipeActionButton *_takeover;
124
+ BOOL _dismissed;
125
+ NSUInteger _generation;
126
+ }
127
+
128
+ - (instancetype)initWithFrame:(NSRect)frame
129
+ {
130
+ if ((self = [super initWithFrame:frame])) {
131
+ _props = std::make_shared<const SwipeActionsProps>();
132
+ _clipView = [[RNSwipeContainerView alloc] initWithFrame:self.bounds];
133
+ _clipView.wantsLayer = YES;
134
+ // Fabric rewrites the host layer's masksToBounds from overflow props. Own this clip separately.
135
+ _clipView.layer.masksToBounds = YES;
136
+ [self addSubview:_clipView];
137
+ _actionsView = [[RNSwipeContainerView alloc] initWithFrame:self.bounds];
138
+ _actionsView.wantsLayer = YES;
139
+ _contentView = [[RNSwipeContainerView alloc] initWithFrame:self.bounds];
140
+ _contentView.wantsLayer = YES;
141
+ [_clipView addSubview:_actionsView];
142
+ [_clipView addSubview:_contentView];
143
+ _leading = @[];
144
+ _trailing = @[];
145
+ _pending = [NSMutableArray new];
146
+ }
147
+ return self;
148
+ }
149
+
150
+ - (BOOL)isFlipped { return YES; }
151
+
152
+ #pragma mark - React
153
+
154
+ - (void)mountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
155
+ {
156
+ if (index < (NSInteger)_contentView.subviews.count) [_contentView addSubview:childComponentView positioned:NSWindowBelow relativeTo:_contentView.subviews[index]];
157
+ else [_contentView addSubview:childComponentView];
158
+ }
159
+
160
+ - (void)unmountChildComponentView:(RCTUIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
161
+ {
162
+ [childComponentView removeFromSuperview];
163
+ }
164
+
165
+ - (NSArray<RNSwipeActionButton *> *)buttonsFromJson:(const std::string &)json edge:(NSInteger)edge
166
+ {
167
+ NSData *data = [NSData dataWithBytes:json.data() length:json.size()];
168
+ NSArray *actions = data.length ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : @[];
169
+ NSMutableArray *buttons = [NSMutableArray new];
170
+ for (NSDictionary *action in [actions isKindOfClass:NSArray.class] ? actions : @[]) {
171
+ RNSwipeActionButton *button = [[RNSwipeActionButton alloc] initWithAction:action];
172
+ button.edge = edge;
173
+ button.owner = self;
174
+ [buttons addObject:button];
175
+ }
176
+ return buttons;
177
+ }
178
+
179
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
180
+ {
181
+ const auto &value = *std::static_pointer_cast<const SwipeActionsProps>(props);
182
+ if (value.leadingActionsJson != _leadingJson || value.trailingActionsJson != _trailingJson) {
183
+ _leadingJson = value.leadingActionsJson;
184
+ _trailingJson = value.trailingActionsJson;
185
+ for (NSView *button in [_leading arrayByAddingObjectsFromArray:_trailing]) [button removeFromSuperview];
186
+ _leading = [self buttonsFromJson:_leadingJson edge:1];
187
+ _trailing = [self buttonsFromJson:_trailingJson edge:-1];
188
+ for (NSView *button in [_leading arrayByAddingObjectsFromArray:_trailing]) [_actionsView addSubview:button];
189
+ [self resetPosition];
190
+ }
191
+ [super updateProps:props oldProps:oldProps];
192
+ // Revealed actions and slid-away content must never paint over neighboring rows.
193
+ self.clipsToBounds = YES;
194
+ }
195
+
196
+ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
197
+ {
198
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
199
+ [self layoutSubviews];
200
+ }
201
+
202
+ - (void)layoutSubviews
203
+ {
204
+ [super layoutSubviews];
205
+ _clipView.frame = self.bounds;
206
+ _actionsView.frame = _clipView.bounds;
207
+ if (_dismissed) _offset = _offset > 0 ? self.bounds.size.width : -self.bounds.size.width;
208
+ [self applyOffset];
209
+ }
210
+
211
+ - (void)prepareForRecycle
212
+ {
213
+ [super prepareForRecycle];
214
+ [self resetPosition];
215
+ }
216
+
217
+ - (void)resetPosition
218
+ {
219
+ // Invalidate delayed action completions before this view acquires a new owner or actions.
220
+ ++_generation;
221
+ if (RNSwipeOpenRow == self) RNSwipeOpenRow = nil;
222
+ [_pending removeAllObjects];
223
+ _gesture = RNSwipeGestureIdle;
224
+ _offset = 0;
225
+ _takeover = nil;
226
+ _dismissed = NO;
227
+ [self applyOffset];
228
+ }
229
+
230
+ + (ComponentDescriptorProvider)componentDescriptorProvider
231
+ {
232
+ return concreteComponentDescriptorProvider<SwipeActionsComponentDescriptor>();
233
+ }
234
+
235
+ #pragma mark - Layout
236
+
237
+ - (void)applyOffset
238
+ {
239
+ NSRect bounds = _clipView.bounds;
240
+ _contentView.frame = NSOffsetRect(bounds, _offset, 0);
241
+ [self layoutButtons:_leading reveal:MAX(0, _offset) bounds:bounds];
242
+ [self layoutButtons:_trailing reveal:MAX(0, -_offset) bounds:bounds];
243
+ }
244
+
245
+ /** The first action sits at the outer edge; a takeover button fills the whole revealed strip. */
246
+ - (void)layoutButtons:(NSArray<RNSwipeActionButton *> *)buttons reveal:(CGFloat)reveal bounds:(NSRect)bounds
247
+ {
248
+ CGFloat height = bounds.size.height, width = bounds.size.width, each = buttons.count ? reveal / buttons.count : 0;
249
+ [buttons enumerateObjectsUsingBlock:^(RNSwipeActionButton *button, NSUInteger index, BOOL *stop) {
250
+ BOOL takesAll = button == self->_takeover, collapsed = !takesAll && [buttons containsObject:self->_takeover];
251
+ CGFloat size = takesAll ? reveal : collapsed ? 0 : each;
252
+ CGFloat inner = takesAll ? 0 : collapsed ? reveal : index * each;
253
+ CGFloat x = button.edge > 0 ? inner : width - inner - size;
254
+ button.followsContent = takesAll;
255
+ button.frame = NSMakeRect(x, 0, size, height);
256
+ button.needsLayout = YES;
257
+ [button layoutSubtreeIfNeeded];
258
+ }];
259
+ }
260
+
261
+ - (NSArray<RNSwipeActionButton *> *)buttonsForOffset:(CGFloat)offset { return offset > 0 ? _leading : offset < 0 ? _trailing : @[]; }
262
+
263
+ - (CGFloat)commitDistance
264
+ {
265
+ CGFloat width = self.bounds.size.width;
266
+ return MIN(MAX(width * 0.45, 150), width - 24);
267
+ }
268
+
269
+ #pragma mark - Gesture
270
+
271
+ - (CGFloat)resisted:(CGFloat)offset
272
+ {
273
+ if ([self buttonsForOffset:offset].count) return MAX(-self.bounds.size.width, MIN(self.bounds.size.width, offset));
274
+ CGFloat distance = fabs(offset);
275
+ return copysign(kRubberLimit * distance / (distance + kRubberLimit), offset);
276
+ }
277
+
278
+ - (void)scrollWheel:(NSEvent *)event
279
+ {
280
+ if (_dismissed || (!_leading.count && !_trailing.count)) { [super scrollWheel:event]; return; }
281
+ // Momentum and plain mouse wheels carry no gesture phase.
282
+ if (event.phase == NSEventPhaseNone || event.phase == NSEventPhaseMayBegin) {
283
+ if (_gesture == RNSwipeGestureTracking && event.momentumPhase != NSEventPhaseNone) return;
284
+ [super scrollWheel:event];
285
+ return;
286
+ }
287
+ if (event.phase == NSEventPhaseBegan) {
288
+ [_pending removeAllObjects];
289
+ _gesture = RNSwipeGestureUndecided;
290
+ _pendingX = 0;
291
+ _pendingY = 0;
292
+ }
293
+ CGFloat dx = event.directionInvertedFromDevice ? event.scrollingDeltaX : -event.scrollingDeltaX;
294
+ BOOL ended = event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled;
295
+
296
+ if (_gesture == RNSwipeGestureUndecided) {
297
+ // Held back until the axis is clear, then replayed to the list if the gesture is vertical.
298
+ [_pending addObject:event];
299
+ _pendingX += dx;
300
+ _pendingY += event.scrollingDeltaY;
301
+ if (!ended && hypot(_pendingX, _pendingY) < kAxisDistance) return;
302
+ if (!ended && fabs(_pendingX) > fabs(_pendingY) * 1.4) {
303
+ _gesture = RNSwipeGestureTracking;
304
+ [_pending removeAllObjects];
305
+ if (RNSwipeOpenRow && RNSwipeOpenRow != self) [RNSwipeOpenRow close];
306
+ [self trackBy:_pendingX];
307
+ return;
308
+ }
309
+ _gesture = RNSwipeGesturePassing;
310
+ if (RNSwipeOpenRow) [RNSwipeOpenRow close];
311
+ NSArray *replay = [_pending copy];
312
+ [_pending removeAllObjects];
313
+ for (NSEvent *pending in replay) [super scrollWheel:pending];
314
+ return;
315
+ }
316
+ if (_gesture == RNSwipeGestureTracking) {
317
+ if (event.phase == NSEventPhaseCancelled) [self close];
318
+ else if (ended) [self settle];
319
+ else [self trackBy:dx];
320
+ return;
321
+ }
322
+ [super scrollWheel:event];
323
+ }
324
+
325
+ - (void)trackBy:(CGFloat)dx
326
+ {
327
+ // Accumulate in unresisted space so rubber-banding does not drift.
328
+ CGFloat raw = _offset + dx;
329
+ if (![self buttonsForOffset:_offset].count && _offset != 0) {
330
+ CGFloat distance = fabs(_offset);
331
+ raw = copysign(kRubberLimit * distance / MAX(kRubberLimit - distance, 1), _offset) + dx;
332
+ }
333
+ _offset = [self resisted:raw];
334
+ NSArray<RNSwipeActionButton *> *buttons = [self buttonsForOffset:_offset];
335
+ CGFloat restDistance = MIN(kButtonWidth * buttons.count, self.bounds.size.width);
336
+ // A closing gesture must never arm just because the resting buttons exceed the threshold.
337
+ RNSwipeActionButton *takeover = fabs(_offset) > restDistance && fabs(_offset) >= [self commitDistance] ? buttons.firstObject : nil;
338
+ if (takeover != _takeover) {
339
+ _takeover = takeover;
340
+ [NSHapticFeedbackManager.defaultPerformer performFeedbackPattern:NSHapticFeedbackPatternAlignment performanceTime:NSHapticFeedbackPerformanceTimeNow];
341
+ RNSwipeAnimate(0.22, ^{ [self applyOffset]; }, nil);
342
+ return;
343
+ }
344
+ [self applyOffset];
345
+ }
346
+
347
+ // The gesture stays Tracking until the next one begins, so its momentum never scrolls the list.
348
+ - (void)settle
349
+ {
350
+ NSArray<RNSwipeActionButton *> *buttons = [self buttonsForOffset:_offset];
351
+ if (_takeover) { [self commit:_takeover]; return; }
352
+ if (buttons.count && fabs(_offset) > kButtonWidth * 0.5) {
353
+ _offset = copysign(MIN(kButtonWidth * buttons.count, self.bounds.size.width), _offset);
354
+ RNSwipeOpenRow = self;
355
+ RNSwipeAnimate(0.34, ^{ [self applyOffset]; }, nil);
356
+ return;
357
+ }
358
+ [self close];
359
+ }
360
+
361
+ - (void)close
362
+ {
363
+ if (RNSwipeOpenRow == self) RNSwipeOpenRow = nil;
364
+ if (_offset == 0 || _dismissed) return;
365
+ _offset = 0;
366
+ _takeover = nil;
367
+ RNSwipeAnimate(0.32, ^{ [self applyOffset]; }, nil);
368
+ }
369
+
370
+ - (void)performActionForButton:(RNSwipeActionButton *)button
371
+ {
372
+ if (_dismissed) return;
373
+ [self commit:button];
374
+ }
375
+
376
+ /** Dismissing actions carry the row away and report once it is gone; others report while it springs back. */
377
+ - (void)commit:(RNSwipeActionButton *)button
378
+ {
379
+ if (RNSwipeOpenRow == self) RNSwipeOpenRow = nil;
380
+ NSString *actionId = button.actionId;
381
+ if (!button.dismisses) {
382
+ [self close];
383
+ [self emit:actionId];
384
+ return;
385
+ }
386
+ _dismissed = YES;
387
+ _takeover = button;
388
+ _offset = button.edge * self.bounds.size.width;
389
+ NSUInteger generation = _generation;
390
+ __weak RNSwipeActions *weakSelf = self;
391
+ RNSwipeAnimate(0.26, ^{ [self applyOffset]; }, ^{
392
+ RNSwipeActions *view = weakSelf;
393
+ if (view && view->_generation == generation && view->_dismissed) [view emit:actionId];
394
+ });
395
+ }
396
+
397
+ - (void)emit:(NSString *)actionId
398
+ {
399
+ auto emitter = std::static_pointer_cast<const SwipeActionsEventEmitter>(_eventEmitter);
400
+ if (emitter) emitter->onSwipeAction({.actionId = std::string(actionId.UTF8String)});
401
+ }
402
+
403
+ #pragma mark - Clicks while open
404
+
405
+ - (NSView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
406
+ {
407
+ NSView *hit = [super hitTest:point withEvent:event];
408
+ // Fabric parents use this local-coordinate route; AppKit's route delegates here too.
409
+ // Respect the superclass visibility/pointer-events result before consuming a close click.
410
+ if (hit && _offset != 0 && !_dismissed && NSPointInRect(point, self.bounds) && NSPointInRect(point, _contentView.frame)) return self;
411
+ return hit;
412
+ }
413
+
414
+ - (BOOL)mouseDownCanMoveWindow { return _offset == 0 && super.mouseDownCanMoveWindow; }
415
+
416
+ - (void)mouseDown:(NSEvent *)event
417
+ {
418
+ if (_offset != 0) [self close];
419
+ else [super mouseDown:event];
420
+ }
421
+
422
+ @end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-ui",
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",
@@ -43,6 +43,7 @@
43
43
  "Sidebar": "RNSidebar",
44
44
  "SidebarItem": "RNSidebarItem",
45
45
  "SFSymbol": "RNSFSymbol",
46
+ "SwipeActions": "RNSwipeActions",
46
47
  "NativeSelect": "RNNativeSelect",
47
48
  "NativeSegmentedControl": "RNNativeSegmentedControl",
48
49
  "TextInputSearch": "RNTextInputSearch"
@@ -68,6 +69,6 @@
68
69
  "dependencies": {
69
70
  "clsx": "^2.1.1",
70
71
  "tailwind-merge": "^3.4.0",
71
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
72
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
72
73
  }
73
74
  }
@@ -9,6 +9,9 @@ export type SidebarSplitViewResizeEvent = Readonly<{
9
9
  height: Double;
10
10
  isLayoutReady: boolean;
11
11
  isVertical: boolean;
12
+ listHeight: Double;
13
+ listWidth: Double;
14
+ listX: Double;
12
15
  sidebarHeight: Double;
13
16
  sidebarWidth: Double;
14
17
  }>;
@@ -20,6 +23,9 @@ export interface NativeProps extends ViewProps {
20
23
  contentTitlebarOverlayColor?: string;
21
24
  contentTitlebarOverlayOpacity?: Double;
22
25
  contentMinWidth?: Double;
26
+ hasList?: boolean;
27
+ listMinWidth?: Double;
28
+ listWidth?: Double;
23
29
  onSplitViewDidResize?: DirectEventHandler<SidebarSplitViewResizeEvent>;
24
30
  sidebarCollapsed?: boolean;
25
31
  sidebarMinWidth?: Double;
@@ -6,8 +6,8 @@ import { appearance, callback, finite, macosViewAvailability, type SpecializedVi
6
6
  import NativeSplitView from "./SidebarSplitViewNativeComponent";
7
7
  export type SidebarSplitViewAppearance = "system" | "light" | "dark";
8
8
  export type SidebarSplitViewTitlebarMaterial = "none" | "glass" | "titlebar" | "headerView" | "hudWindow" | "sidebar" | "windowBackground";
9
- export interface SidebarSplitViewPaneMetrics { contentHeight: number; contentWidth: number; sidebarHeight: number; sidebarWidth: number }
10
- export interface SidebarSplitViewResizeEvent extends SidebarSplitViewPaneMetrics { contentX: number; height: number; phase: "provisional" | "ready" }
9
+ export interface SidebarSplitViewPaneMetrics { contentHeight: number; contentWidth: number; sidebarHeight: number; sidebarWidth: number; listHeight?: number; listWidth?: number }
10
+ export interface SidebarSplitViewResizeEvent extends SidebarSplitViewPaneMetrics { listHeight: number; listWidth: number; contentX: number; listX: number; height: number; phase: "provisional" | "ready" }
11
11
  export interface SplitViewTitleBarOverlay { color: string; opacity?: number }
12
12
  export interface SplitViewTitleBarOptions {
13
13
  content?: { height?: number; material?: SidebarSplitViewTitlebarMaterial; overlay?: SplitViewTitleBarOverlay };
@@ -16,11 +16,15 @@ export interface SplitViewTitleBarOptions {
16
16
  export interface SidebarSplitViewProps extends Omit<SpecializedViewProps, "children"> {
17
17
  sidebar: ReactNode;
18
18
  content: ReactNode;
19
+ /** Optional middle column (AppKit content list, as in Mail): sidebar | list | content in one native split. */
20
+ list?: ReactNode;
19
21
  children?: never;
20
22
  appearance?: SidebarSplitViewAppearance;
21
23
  contentMinWidth?: number;
22
24
  sidebarMinWidth?: number;
23
25
  sidebarWidth?: number;
26
+ listMinWidth?: number;
27
+ listWidth?: number;
24
28
  sidebarCollapsed?: boolean;
25
29
  /** Used only for initial layout, before native measurements arrive. */
26
30
  initialPaneMetrics?: SidebarSplitViewPaneMetrics;
@@ -31,7 +35,9 @@ export function getSplitViewAvailability() { return macosViewAvailability("Sideb
31
35
  function metrics(value: SidebarSplitViewPaneMetrics) {
32
36
  if (!value || typeof value !== "object") throw new SparkError("E_INVALID_ARGUMENT", "Expected pane metrics");
33
37
  for (const key of ["contentHeight", "contentWidth", "sidebarHeight", "sidebarWidth"] as const) finite(value[key], key);
38
+ for (const key of ["listHeight", "listWidth"] as const) if (value[key] !== undefined) finite(value[key], key);
34
39
  }
40
+ const paneKeys = ["contentHeight", "contentWidth", "sidebarHeight", "sidebarWidth", "listHeight", "listWidth"] as const;
35
41
  function keys(value: unknown, allowed: readonly string[]) {
36
42
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new SparkError("E_INVALID_ARGUMENT", "Expected title-bar options");
37
43
  for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new SparkError("E_UNSUPPORTED_OPTION", `Unknown title-bar option: ${key}`);
@@ -42,9 +48,11 @@ function overlay(value?: SplitViewTitleBarOverlay) {
42
48
  if (typeof value.color !== "string" || !/^#(?:[\da-f]{6}|[\da-f]{8})$/i.test(value.color)) throw new SparkError("E_INVALID_ARGUMENT", "Expected #RRGGBB or #RRGGBBAA title-bar color");
43
49
  if (value.opacity !== undefined) finite(value.opacity, "overlay opacity", 0, 1);
44
50
  }
45
- export function SidebarSplitView({ sidebar, content, children, appearance: theme = "system", contentMinWidth = 320, sidebarMinWidth = 180, sidebarWidth, sidebarCollapsed = false, initialPaneMetrics, titleBar = {}, onResize, ref, onError, ...props }: SidebarSplitViewProps) {
46
- appearance(theme); finite(contentMinWidth, "minimum content width"); finite(sidebarMinWidth, "minimum sidebar width");
51
+ export function SidebarSplitView({ sidebar, content, list, children, appearance: theme = "system", contentMinWidth = 320, sidebarMinWidth = 180, sidebarWidth, listMinWidth = 240, listWidth, sidebarCollapsed = false, initialPaneMetrics, titleBar = {}, onResize, ref, onError, ...props }: SidebarSplitViewProps) {
52
+ appearance(theme); finite(contentMinWidth, "minimum content width"); finite(sidebarMinWidth, "minimum sidebar width"); finite(listMinWidth, "minimum list width");
47
53
  if (sidebarWidth !== undefined) finite(sidebarWidth, "sidebar width", sidebarMinWidth);
54
+ if (listWidth !== undefined) finite(listWidth, "list width", listMinWidth);
55
+ const hasList = list !== undefined && list !== null && typeof list !== "boolean" && list !== "";
48
56
  if (typeof sidebarCollapsed !== "boolean" || children !== undefined) throw new SparkError("E_INVALID_ARGUMENT", "Use named sidebar/content panes and a boolean collapsed state");
49
57
  if (initialPaneMetrics !== undefined) metrics(initialPaneMetrics);
50
58
  keys(titleBar, ["content", "sidebar"]);
@@ -56,22 +64,23 @@ export function SidebarSplitView({ sidebar, content, children, appearance: theme
56
64
  callback(onResize, "resize");
57
65
  const control = useControl({ ref, onError }, getSplitViewAvailability());
58
66
  const [paneMetrics, setPaneMetrics] = useState<SidebarSplitViewPaneMetrics | undefined>(() => initialPaneMetrics ? { ...initialPaneMetrics } : undefined);
59
- function resize(event: NativeSyntheticEvent<SidebarSplitViewPaneMetrics & { contentX: number; height: number; isLayoutReady: boolean }>) {
67
+ function resize(event: NativeSyntheticEvent<SidebarSplitViewPaneMetrics & { contentX: number; listX: number; height: number; isLayoutReady: boolean }>) {
60
68
  if (!control.active()) return;
61
69
  let value: SidebarSplitViewResizeEvent;
62
70
  try {
63
- const data = event?.nativeEvent; metrics(data); finite(data.contentX, "content x", -Infinity); finite(data.height, "height");
71
+ const data = event?.nativeEvent; metrics(data); finite(data.listHeight, "list height"); finite(data.listWidth, "list width"); finite(data.contentX, "content x", -Infinity); finite(data.listX, "list x", -Infinity); finite(data.height, "height");
64
72
  if (typeof data.isLayoutReady !== "boolean") throw new Error("Expected layout readiness");
65
- value = { contentHeight: data.contentHeight, contentWidth: data.contentWidth, sidebarHeight: data.sidebarHeight, sidebarWidth: data.sidebarWidth, contentX: data.contentX, height: data.height, phase: data.isLayoutReady ? "ready" : "provisional" };
73
+ value = { contentHeight: data.contentHeight, contentWidth: data.contentWidth, sidebarHeight: data.sidebarHeight, sidebarWidth: data.sidebarWidth, listHeight: data.listHeight, listWidth: data.listWidth, contentX: data.contentX, listX: data.listX, height: data.height, phase: data.isLayoutReady ? "ready" : "provisional" };
66
74
  } catch (cause) { control.error(new SparkError("E_INVALID_DATA", "Invalid split-view layout", { cause })); return; }
67
- setPaneMetrics(current => current && ["contentHeight", "contentWidth", "sidebarHeight", "sidebarWidth"].every(key => current[key as keyof SidebarSplitViewPaneMetrics] === value[key as keyof SidebarSplitViewPaneMetrics]) ? current : value);
75
+ setPaneMetrics(current => current && paneKeys.every(key => current[key] === value[key]) ? current : value);
68
76
  onResize?.(value);
69
77
  }
70
- if (control.failed) return <View {...props} ref={control.ref} style={[{ flexDirection: "row" }, props.style]}>{sidebarCollapsed ? null : sidebar}{content}</View>;
71
- return <NativeSplitView {...props} ref={control.ref} appearance={theme} contentMinWidth={contentMinWidth} sidebarMinWidth={sidebarMinWidth} sidebarWidth={sidebarWidth} sidebarCollapsed={sidebarCollapsed} onSplitViewDidResize={resize}
78
+ if (control.failed) return <View {...props} ref={control.ref} style={[{ flexDirection: "row" }, props.style]}>{sidebarCollapsed ? null : sidebar}{list}{content}</View>;
79
+ return <NativeSplitView {...props} ref={control.ref} appearance={theme} contentMinWidth={contentMinWidth} sidebarMinWidth={sidebarMinWidth} sidebarWidth={sidebarWidth} hasList={hasList} listMinWidth={listMinWidth} listWidth={listWidth} sidebarCollapsed={sidebarCollapsed} onSplitViewDidResize={resize}
72
80
  contentTitlebarHeight={titleBar.content?.height ?? 0} contentTitlebarMaterial={material} contentTitlebarOverlayColor={titleBar.content?.overlay?.color} contentTitlebarOverlayOpacity={titleBar.content?.overlay?.opacity ?? (titleBar.content?.overlay ? 1 : 0)}
73
81
  sidebarTitlebarOverlayColor={titleBar.sidebar?.overlay?.color} sidebarTitlebarOverlayOpacity={titleBar.sidebar?.overlay?.opacity ?? (titleBar.sidebar?.overlay ? 1 : 0)}>
74
82
  <View key="sidebar" style={{ position: "absolute", top: 0, left: 0, minWidth: 0, height: paneMetrics?.sidebarHeight, width: sidebarCollapsed ? 0 : paneMetrics?.sidebarWidth ?? sidebarWidth ?? sidebarMinWidth }}>{sidebar}</View>
75
- <View key="content" style={{ position: "absolute", top: 0, left: 0, minWidth: 0, height: paneMetrics?.contentHeight, width: paneMetrics?.contentWidth }}>{content}</View>
83
+ <View key="content" style={{ position: "absolute", top: 0, left: 0, minWidth: 0, overflow: "hidden", height: paneMetrics?.contentHeight, width: paneMetrics?.contentWidth }}>{content}</View>
84
+ {hasList ? <View key="list" style={{ position: "absolute", top: 0, left: 0, minWidth: 0, height: paneMetrics?.listHeight, width: paneMetrics?.listWidth ?? listWidth ?? listMinWidth }}>{list}</View> : null}
76
85
  </NativeSplitView>;
77
86
  }
@@ -0,0 +1,11 @@
1
+ import type { HostComponent, ViewProps } from "react-native";
2
+ import { codegenNativeComponent } from "react-native";
3
+ import type { DirectEventHandler } from "react-native/Libraries/Types/CodegenTypes";
4
+
5
+ export interface NativeProps extends ViewProps {
6
+ leadingActionsJson: string;
7
+ trailingActionsJson: string;
8
+ onSwipeAction?: DirectEventHandler<Readonly<{ actionId: string }>>;
9
+ }
10
+
11
+ export default codegenNativeComponent<NativeProps>("SwipeActions") as HostComponent<NativeProps>;
@@ -0,0 +1,41 @@
1
+ import { View, type NativeSyntheticEvent } from "react-native";
2
+ import { SparkError } from "@legendapp/spark-desktop-app/src/contracts";
3
+ import { useControl } from "../control";
4
+ import { identifier, macosViewAvailability, type SpecializedViewProps } from "../specialized";
5
+ import NativeSwipeActions from "./SwipeActionsNativeComponent";
6
+ /** `dismisses` slides the row away on commit; remove or remount the row to recover after failure/undo. */
7
+ export interface SwipeAction { id: string; title: string; symbol: string; color: string; dismisses?: boolean }
8
+ export interface SwipeActionsProps extends SpecializedViewProps {
9
+ /** Revealed by swiping right; the first action is outermost and runs on a full swipe. */
10
+ leadingActions?: readonly SwipeAction[];
11
+ /** Revealed by swiping left; the first action is outermost and runs on a full swipe. */
12
+ trailingActions?: readonly SwipeAction[];
13
+ onAction: (id: string) => void;
14
+ }
15
+ const hexColor = /^#(?:[0-9a-f]{6}|[0-9a-f]{8})$/i;
16
+ function actionsJson(actions: readonly SwipeAction[], ids: Set<string>) {
17
+ if (!Array.isArray(actions)) throw new SparkError("E_INVALID_ARGUMENT", "Swipe actions must be arrays");
18
+ for (const action of actions) {
19
+ identifier(action?.id, "swipe action ID"); identifier(action.title, "swipe action title"); identifier(action.symbol, "swipe action symbol");
20
+ if (typeof action.color !== "string" || !hexColor.test(action.color)) throw new SparkError("E_INVALID_ARGUMENT", "Swipe action colors are #RRGGBB or #RRGGBBAA");
21
+ if (action.dismisses !== undefined && typeof action.dismisses !== "boolean") throw new SparkError("E_INVALID_ARGUMENT", "Invalid swipe action dismisses");
22
+ if (ids.has(action.id)) throw new SparkError("E_INVALID_ARGUMENT", "Duplicate swipe action ID");
23
+ ids.add(action.id);
24
+ }
25
+ return JSON.stringify(actions.map(({ id, title, symbol, color, dismisses = false }) => ({ id, title, symbol, color, dismisses })));
26
+ }
27
+ export function getSwipeActionsAvailability() { return macosViewAvailability("SwipeActions"); }
28
+ /** Native trackpad swipe actions. In recycling lists, key this view by the logical row ID. */
29
+ export function SwipeActions({ leadingActions = [], trailingActions = [], onAction, ref, onError, ...props }: SwipeActionsProps) {
30
+ if (typeof onAction !== "function") throw new SparkError("E_INVALID_ARGUMENT", "Expected swipe action callback");
31
+ const ids = new Set<string>(), leading = actionsJson(leadingActions, ids), trailing = actionsJson(trailingActions, ids);
32
+ const control = useControl({ ref, onError }, getSwipeActionsAvailability());
33
+ function action(event: NativeSyntheticEvent<{ actionId: string }>) {
34
+ if (!control.active()) return;
35
+ const id = event?.nativeEvent?.actionId;
36
+ if (typeof id !== "string" || !ids.has(id)) { control.error(new SparkError("E_INVALID_DATA", "Invalid swipe action")); return; }
37
+ onAction(id);
38
+ }
39
+ if (control.failed) return <View {...props} ref={control.ref} />;
40
+ return <NativeSwipeActions {...props} ref={control.ref} leadingActionsJson={leading} trailingActionsJson={trailing} onSwipeAction={action} />;
41
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-updates",
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",
@@ -38,7 +38,7 @@
38
38
  ]
39
39
  },
40
40
  "dependencies": {
41
- "@legendapp/spark-desktop-app": "0.0.1-next.3"
41
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
42
42
  },
43
43
  "private": true
44
44
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-webview",
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",
@@ -9,7 +9,7 @@
9
9
  ],
10
10
  "dependencies": {
11
11
  "react-native-webview": "16.0.0",
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-window-options",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "index.cjs",
6
6
  "types": "index.d.ts",