panelui-native 0.49.0 → 0.51.1
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/README.md +8 -1
- package/lib/module/components/sortable/index.js +806 -0
- package/lib/module/components/sortable/index.js.map +1 -0
- package/lib/module/components/swipe/index.js +140 -5
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/icons/index.js +29 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/haptics.js +19 -0
- package/lib/module/utils/haptics.js.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts +238 -0
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts +35 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +8 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +14 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/sortable/index.tsx +1080 -0
- package/src/components/swipe/index.tsx +165 -3
- package/src/icons/index.tsx +22 -0
- package/src/index.ts +12 -0
- package/src/utils/haptics.ts +21 -0
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Everything that moves runs on the UI thread. A row being dragged does not
|
|
29
29
|
* re-render — the only React work in a swipe is the callback at the end of it.
|
|
30
|
+
*
|
|
31
|
+
* Rows in a `Swipe.Group` close each other, so only one of them is ever open.
|
|
32
|
+
* That is the behaviour of every list on the phone that has this gesture, and
|
|
33
|
+
* a row cannot arrange it alone: it knows when it opens and has no way to hear
|
|
34
|
+
* that a sibling did.
|
|
30
35
|
*/
|
|
31
36
|
import {
|
|
32
37
|
Children,
|
|
@@ -36,6 +41,7 @@ import {
|
|
|
36
41
|
isValidElement,
|
|
37
42
|
useCallback,
|
|
38
43
|
useContext,
|
|
44
|
+
useEffect,
|
|
39
45
|
useImperativeHandle,
|
|
40
46
|
useMemo,
|
|
41
47
|
useRef,
|
|
@@ -96,6 +102,131 @@ interface SwipeContextValue {
|
|
|
96
102
|
|
|
97
103
|
const SwipeContext = createContext<SwipeContextValue | null>(null);
|
|
98
104
|
|
|
105
|
+
/* -------------------------------------------------------------------------- */
|
|
106
|
+
/* Group */
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
interface SwipeGroupContextValue {
|
|
110
|
+
/** Take a slot in the group. Returns the function that gives it back. */
|
|
111
|
+
join: (row: SwipeGroupMember) => () => void;
|
|
112
|
+
/** Say that this row has opened, so every other one can put itself away. */
|
|
113
|
+
opened: (row: SwipeGroupMember) => void;
|
|
114
|
+
/** Shut every row in the group. */
|
|
115
|
+
closeAll: () => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SwipeGroupMember {
|
|
119
|
+
close: () => void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const SwipeGroupContext = createContext<SwipeGroupContextValue | null>(null);
|
|
123
|
+
|
|
124
|
+
export interface SwipeGroupProps extends ViewProps {
|
|
125
|
+
className?: string;
|
|
126
|
+
children?: ReactNode;
|
|
127
|
+
/**
|
|
128
|
+
* Close the other rows when one opens. On by default — that is the whole
|
|
129
|
+
* reason to reach for a group. Turning it off keeps the container and the
|
|
130
|
+
* `useSwipeGroup` handle while letting several rows stand open at once.
|
|
131
|
+
*/
|
|
132
|
+
exclusive?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Several rows that agree only one of them is open at a time.
|
|
137
|
+
*
|
|
138
|
+
* Every list of swipeable rows wants this, and every list has to be told: a
|
|
139
|
+
* row knows when it opens but has no way to learn that a sibling did, so
|
|
140
|
+
* without something above them a list ends up with three rows standing open
|
|
141
|
+
* and a screen that reads as broken. The mail, message and reminder lists on
|
|
142
|
+
* a phone all behave this way, and a list that does not is the odd one out.
|
|
143
|
+
*
|
|
144
|
+
* ```tsx
|
|
145
|
+
* <Swipe.Group>
|
|
146
|
+
* {rows.map((row) => (
|
|
147
|
+
* <Swipe key={row.id}>
|
|
148
|
+
* <Swipe.End>
|
|
149
|
+
* <Swipe.Action label="Delete" color="destructive" onPress={…} />
|
|
150
|
+
* </Swipe.End>
|
|
151
|
+
* <Item>…</Item>
|
|
152
|
+
* </Swipe>
|
|
153
|
+
* ))}
|
|
154
|
+
* </Swipe.Group>
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* The rows register themselves rather than being found by inspecting children,
|
|
158
|
+
* so a row nested inside anything at all still belongs — wrapped in an
|
|
159
|
+
* `Item.Group`, produced by a `map`, rendered by a component of your own. The
|
|
160
|
+
* alternative, walking the tree for `Swipe` elements, only ever works for the
|
|
161
|
+
* one arrangement it was written against.
|
|
162
|
+
*
|
|
163
|
+
* Nothing here re-renders. The registry is a ref and closing a sibling writes
|
|
164
|
+
* to that row's shared value, so opening a row costs the springs it starts and
|
|
165
|
+
* no React work at all.
|
|
166
|
+
*/
|
|
167
|
+
const SwipeGroup = forwardRef<View, SwipeGroupProps>(
|
|
168
|
+
({ className, children, exclusive = true, ...props }, ref) => {
|
|
169
|
+
/*
|
|
170
|
+
* A ref rather than state: membership changes as rows mount and unmount,
|
|
171
|
+
* and rendering the whole list again because one row arrived would undo
|
|
172
|
+
* the point of a component that never re-renders while it is dragged.
|
|
173
|
+
*/
|
|
174
|
+
const members = useRef(new Set<SwipeGroupMember>());
|
|
175
|
+
|
|
176
|
+
const context = useMemo<SwipeGroupContextValue>(
|
|
177
|
+
() => ({
|
|
178
|
+
join: (row) => {
|
|
179
|
+
members.current.add(row);
|
|
180
|
+
return () => {
|
|
181
|
+
members.current.delete(row);
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
opened: (row) => {
|
|
185
|
+
if (!exclusive) return;
|
|
186
|
+
for (const other of members.current) {
|
|
187
|
+
if (other !== row) other.close();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
closeAll: () => {
|
|
191
|
+
for (const row of members.current) row.close();
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
[exclusive]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<SwipeGroupContext.Provider value={context}>
|
|
199
|
+
<View ref={ref} className={className} {...props}>
|
|
200
|
+
{children}
|
|
201
|
+
</View>
|
|
202
|
+
</SwipeGroupContext.Provider>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
SwipeGroup.displayName = 'Swipe.Group';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Shut every row in the enclosing `Swipe.Group`.
|
|
210
|
+
*
|
|
211
|
+
* The one thing a group knows that a single row cannot: a list that scrolls,
|
|
212
|
+
* navigates away, or has just deleted the row that was open wants all of them
|
|
213
|
+
* put back, and holding a ref to each row to do it by hand is bookkeeping the
|
|
214
|
+
* group is already doing.
|
|
215
|
+
*
|
|
216
|
+
* ```tsx
|
|
217
|
+
* const { closeAll } = useSwipeGroup();
|
|
218
|
+
* <ScrollView onScrollBeginDrag={closeAll}>…</ScrollView>
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* Outside a group it is inert rather than an error, so a row that is sometimes
|
|
222
|
+
* grouped and sometimes not does not need two versions of its parent.
|
|
223
|
+
*/
|
|
224
|
+
export function useSwipeGroup(): { closeAll: () => void } {
|
|
225
|
+
const group = useContext(SwipeGroupContext);
|
|
226
|
+
const closeAll = useCallback(() => group?.closeAll(), [group]);
|
|
227
|
+
return { closeAll };
|
|
228
|
+
}
|
|
229
|
+
|
|
99
230
|
/* -------------------------------------------------------------------------- */
|
|
100
231
|
/* Action */
|
|
101
232
|
/* -------------------------------------------------------------------------- */
|
|
@@ -440,6 +571,35 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
440
571
|
reportOpen(null);
|
|
441
572
|
}, [offset, reportOpen]);
|
|
442
573
|
|
|
574
|
+
/*
|
|
575
|
+
* Membership in a `Swipe.Group`, if there is one above this row.
|
|
576
|
+
*
|
|
577
|
+
* The registered member is a stable object rather than the `close`
|
|
578
|
+
* function itself, because `close` changes identity whenever
|
|
579
|
+
* `onOpenChange` does and the group would then be holding a stale entry
|
|
580
|
+
* alongside a live one. The object never changes; what it closes is read
|
|
581
|
+
* off a ref at the moment it is asked.
|
|
582
|
+
*/
|
|
583
|
+
const group = useContext(SwipeGroupContext);
|
|
584
|
+
const closeRef = useRef(close);
|
|
585
|
+
closeRef.current = close;
|
|
586
|
+
const member = useRef<SwipeGroupMember>({ close: () => closeRef.current() }).current;
|
|
587
|
+
|
|
588
|
+
useEffect(() => group?.join(member), [group, member]);
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Told when this row settles open, so the group can put its siblings away.
|
|
592
|
+
* A row closing says nothing — the others are already closed, and telling
|
|
593
|
+
* them so would be a round of work per settle for no change.
|
|
594
|
+
*/
|
|
595
|
+
const announce = useCallback(
|
|
596
|
+
(side: SwipeOpenSide) => {
|
|
597
|
+
reportOpen(side);
|
|
598
|
+
if (side !== null) group?.opened(member);
|
|
599
|
+
},
|
|
600
|
+
[reportOpen, group, member]
|
|
601
|
+
);
|
|
602
|
+
|
|
443
603
|
useImperativeHandle(
|
|
444
604
|
ref,
|
|
445
605
|
() => ({
|
|
@@ -447,11 +607,11 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
447
607
|
const width = side === 'start' ? startWidth.value : endWidth.value;
|
|
448
608
|
if (width === 0) return;
|
|
449
609
|
offset.value = withSpring(side === 'start' ? width : -width, SPRING);
|
|
450
|
-
|
|
610
|
+
announce(side);
|
|
451
611
|
},
|
|
452
612
|
close,
|
|
453
613
|
}),
|
|
454
|
-
[offset, startWidth, endWidth,
|
|
614
|
+
[offset, startWidth, endWidth, announce, close]
|
|
455
615
|
);
|
|
456
616
|
|
|
457
617
|
/**
|
|
@@ -541,7 +701,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
541
701
|
|
|
542
702
|
if (projected > limit * OPEN_RATIO) {
|
|
543
703
|
offset.value = withSpring(toStart ? limit : -limit, SPRING);
|
|
544
|
-
runOnJS(
|
|
704
|
+
runOnJS(announce)(side);
|
|
545
705
|
} else {
|
|
546
706
|
offset.value = withSpring(0, SPRING);
|
|
547
707
|
runOnJS(reportOpen)(null);
|
|
@@ -562,6 +722,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
562
722
|
fire,
|
|
563
723
|
tick,
|
|
564
724
|
reportOpen,
|
|
725
|
+
announce,
|
|
565
726
|
]
|
|
566
727
|
);
|
|
567
728
|
|
|
@@ -681,6 +842,7 @@ const SwipeRoot = forwardRef<SwipeHandle, SwipeProps>(
|
|
|
681
842
|
SwipeRoot.displayName = 'Swipe';
|
|
682
843
|
|
|
683
844
|
export const Swipe = Object.assign(SwipeRoot, {
|
|
845
|
+
Group: SwipeGroup,
|
|
684
846
|
Start: SwipeStart,
|
|
685
847
|
End: SwipeEnd,
|
|
686
848
|
Action: SwipeAction,
|
package/src/icons/index.tsx
CHANGED
|
@@ -991,6 +991,28 @@ export function TrashIcon({ size = 16, color, ...props }: IconProps) {
|
|
|
991
991
|
);
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Two columns of dots — the grip on a row that can be dragged somewhere else.
|
|
996
|
+
*
|
|
997
|
+
* Dots rather than the stacked rules of a menu glyph, and two columns rather
|
|
998
|
+
* than one: the pattern has to read as a surface to take hold of from either
|
|
999
|
+
* side, and a single column reads as a divider instead.
|
|
1000
|
+
*/
|
|
1001
|
+
export function GripVerticalIcon({ size = 16, color, ...props }: IconProps) {
|
|
1002
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1003
|
+
return (
|
|
1004
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1005
|
+
<Path
|
|
1006
|
+
d="M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01"
|
|
1007
|
+
stroke={resolved}
|
|
1008
|
+
strokeWidth={2.5}
|
|
1009
|
+
strokeLinecap="round"
|
|
1010
|
+
strokeLinejoin="round"
|
|
1011
|
+
/>
|
|
1012
|
+
</Svg>
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
994
1016
|
/** An arrow into a tray — save the thing to a file. */
|
|
995
1017
|
export function DownloadIcon({ size = 16, color, ...props }: IconProps) {
|
|
996
1018
|
const resolved = useResolvedColor(color, '#737373');
|
package/src/index.ts
CHANGED
|
@@ -796,6 +796,16 @@ export {
|
|
|
796
796
|
} from './components/thinking-orb';
|
|
797
797
|
export { Skeleton, type SkeletonProps } from './components/skeleton';
|
|
798
798
|
export { Slider, type SliderProps } from './components/slider';
|
|
799
|
+
export {
|
|
800
|
+
Sortable,
|
|
801
|
+
reorderItems,
|
|
802
|
+
useSortableItem,
|
|
803
|
+
type SortableProps,
|
|
804
|
+
type SortableItemProps,
|
|
805
|
+
type SortableHandleProps,
|
|
806
|
+
type SortableActivation,
|
|
807
|
+
type SortableReorderDetails,
|
|
808
|
+
} from './components/sortable';
|
|
799
809
|
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
800
810
|
export {
|
|
801
811
|
Steps,
|
|
@@ -810,7 +820,9 @@ export {
|
|
|
810
820
|
export { Surface, type SurfaceProps } from './components/surface';
|
|
811
821
|
export {
|
|
812
822
|
Swipe,
|
|
823
|
+
useSwipeGroup,
|
|
813
824
|
type SwipeProps,
|
|
825
|
+
type SwipeGroupProps,
|
|
814
826
|
type SwipePanelProps,
|
|
815
827
|
type SwipeActionProps,
|
|
816
828
|
type SwipeActionColor,
|
package/src/utils/haptics.ts
CHANGED
|
@@ -61,3 +61,24 @@ export function hasHaptics(): boolean {
|
|
|
61
61
|
export function selectionTick(): void {
|
|
62
62
|
void getHaptics()?.selectionAsync().catch(() => {});
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The knock for a thing coming loose or landing — a row lifted out of a list,
|
|
67
|
+
* a card dropped into place.
|
|
68
|
+
*
|
|
69
|
+
* An impact rather than a selection because it marks a change of state rather
|
|
70
|
+
* than a change of value: the finger is now holding something it was not
|
|
71
|
+
* holding a moment ago, and the lightest feedback is not enough to say so.
|
|
72
|
+
* `medium` is the one that reads as picking something up without reading as an
|
|
73
|
+
* error.
|
|
74
|
+
*
|
|
75
|
+
* Falls back to a selection tick where the style constants are missing, and to
|
|
76
|
+
* nothing at all where the package is.
|
|
77
|
+
*/
|
|
78
|
+
export function impactKnock(): void {
|
|
79
|
+
const haptics = getHaptics();
|
|
80
|
+
if (!haptics) return;
|
|
81
|
+
void haptics
|
|
82
|
+
.impactAsync(haptics.ImpactFeedbackStyle?.Medium)
|
|
83
|
+
.catch(() => {});
|
|
84
|
+
}
|