@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.
- package/camera.ts +13 -0
- package/package.json +21 -11
- package/patches/camera/react-native-nitro-image.patch +320 -0
- package/patches/camera/react-native-nitro-modules.patch +50 -0
- package/patches/camera/react-native-vision-camera.patch +2662 -0
- package/patches/camera/upstream.json +22 -0
- package/patches/dependencies.json +8 -0
- package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
- package/postinstall.cjs +1 -0
- package/ui-swipe.ts +1 -0
- package/vendor/node_modules/@legendapp/spark-ai/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-audio/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-auth-session/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +62 -11
- package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +3 -2
- package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +6 -3
- package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +41 -26
- package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +43 -7
- package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +3 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +27 -22
- package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +19 -19
- package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +1 -8
- package/vendor/node_modules/@legendapp/spark-cli/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-codex/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-commands/package.json +5 -5
- package/vendor/node_modules/@legendapp/spark-context-menu/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +1 -1
- package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +6 -4
- package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +2 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +5 -4
- package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +39 -0
- package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +3 -1
- package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +1 -1
- package/vendor/node_modules/@legendapp/spark-documents/package.json +7 -7
- package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-file-system/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-native-menu/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +16 -11
- package/vendor/node_modules/@legendapp/spark-processes/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-settings/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +4 -4
- package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +263 -26
- package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
- package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
- package/vendor/node_modules/@legendapp/spark-ui/package.json +3 -2
- package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +6 -0
- package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +20 -11
- package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
- package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
- package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
- 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
|
+
"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.
|
|
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 &&
|
|
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
|
}
|
package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts
ADDED
|
@@ -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
|
+
"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.
|
|
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
|
+
"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.
|
|
12
|
+
"@legendapp/spark-desktop-app": "0.0.1-next.4"
|
|
13
13
|
},
|
|
14
14
|
"private": true
|
|
15
15
|
}
|