panelui-native 0.40.0 → 0.43.0
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 +4 -2
- package/lib/module/components/card/index.js +172 -3
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/color-picker/index.js +1395 -0
- package/lib/module/components/color-picker/index.js.map +1 -0
- package/lib/module/components/combobox/index.js +87 -19
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +13 -4
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/kpi-chart/index.js +648 -0
- package/lib/module/components/kpi-chart/index.js.map +1 -0
- package/lib/module/components/map/index.js +72 -12
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/menu/index.js +40 -10
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/pagination/index.js +19 -9
- package/lib/module/components/pagination/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +68 -18
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +704 -0
- package/lib/module/components/radar-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +32 -7
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/slider/index.js +240 -39
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +194 -10
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/text-animation/index.js +857 -0
- package/lib/module/components/text-animation/index.js.map +1 -0
- package/lib/module/icons/index.js +25 -7
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/module/utils/chart.js +44 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/module/utils/color.js +268 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/typescript/src/components/card/index.d.ts +20 -0
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/color-picker/index.d.ts +258 -0
- package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +13 -1
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi-chart/index.d.ts +490 -0
- package/lib/typescript/src/components/kpi-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +37 -2
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +50 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radar-chart/index.d.ts +214 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +19 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +11 -1
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
- package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +6 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +8 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +21 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/lib/typescript/src/utils/color.d.ts +74 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/components/card/index.tsx +227 -2
- package/src/components/color-picker/index.tsx +1618 -0
- package/src/components/combobox/index.tsx +89 -19
- package/src/components/drawer/index.tsx +31 -9
- package/src/components/kpi-chart/index.tsx +719 -0
- package/src/components/map/index.tsx +158 -14
- package/src/components/menu/index.tsx +51 -6
- package/src/components/pagination/index.tsx +18 -6
- package/src/components/panelside/index.tsx +134 -19
- package/src/components/radar-chart/index.tsx +910 -0
- package/src/components/section-rail/index.tsx +42 -8
- package/src/components/slider/index.tsx +283 -41
- package/src/components/tabs/index.tsx +263 -8
- package/src/components/text-animation/index.tsx +1093 -0
- package/src/icons/index.tsx +23 -4
- package/src/index.ts +73 -1
- package/src/theme/use-theme.ts +1 -1
- package/src/utils/chart.ts +52 -0
- package/src/utils/color.ts +246 -0
- package/theme.css +125 -112
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
View,
|
|
64
64
|
type LayoutChangeEvent,
|
|
65
65
|
type ViewProps,
|
|
66
|
+
type ViewStyle,
|
|
66
67
|
} from 'react-native';
|
|
67
68
|
import Animated, {
|
|
68
69
|
FadeIn,
|
|
@@ -103,7 +104,13 @@ const comboboxVariants = tv({
|
|
|
103
104
|
// Chips wrap onto their own lines; the input keeps a sane minimum so it is
|
|
104
105
|
// still tappable once a few of them are in front of it.
|
|
105
106
|
fieldContent: 'flex-1 flex-row flex-wrap items-center gap-1.5 py-1',
|
|
106
|
-
|
|
107
|
+
/*
|
|
108
|
+
* A fixed height rather than vertical padding, because the chips beside it
|
|
109
|
+
* have one (`h-6`) and two differently-sized boxes on a `items-center` row
|
|
110
|
+
* centre to two different baselines. `py-0` clears the platform default,
|
|
111
|
+
* which is not the same on iOS and Android.
|
|
112
|
+
*/
|
|
113
|
+
input: 'h-7 min-w-24 flex-1 py-0 text-base font-normal text-foreground',
|
|
107
114
|
action: 'h-6 w-6 items-center justify-center rounded-full',
|
|
108
115
|
list: 'overflow-hidden rounded-xl border border-border bg-popover p-2 shadow-sm',
|
|
109
116
|
item: 'flex-row items-center gap-2 rounded-lg px-3 py-3',
|
|
@@ -190,6 +197,9 @@ function ComboboxItem({
|
|
|
190
197
|
accessibilityState={{ selected, disabled: !!disabled }}
|
|
191
198
|
disabled={disabled}
|
|
192
199
|
onPress={() => context.onSelect(value, label)}
|
|
200
|
+
// The rows sit flush against each other, so the 4pt either side of the
|
|
201
|
+
// gap between two of them belongs to neither without this.
|
|
202
|
+
hitSlop={{ top: 2, bottom: 2 }}
|
|
193
203
|
className={item()}
|
|
194
204
|
>
|
|
195
205
|
{start}
|
|
@@ -413,7 +423,17 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
413
423
|
const [open, setOpen] = useState(false);
|
|
414
424
|
const [focused, setFocused] = useState(false);
|
|
415
425
|
const [anchor, setAnchor] = useState<Anchor | null>(null);
|
|
416
|
-
|
|
426
|
+
/*
|
|
427
|
+
* Which side the list settled on, latched for as long as it stays open.
|
|
428
|
+
*
|
|
429
|
+
* The side is decided from the list's measured height, which is 0 on the
|
|
430
|
+
* frame it opens — so the honest answer changes once, from "below" to
|
|
431
|
+
* whatever fits, and the list jumps. A finger already on its way down to an
|
|
432
|
+
* option lands where the option used to be, which is the whole of "sometimes
|
|
433
|
+
* it takes two taps". Deciding once and holding it costs a list that opens
|
|
434
|
+
* downwards for one frame in a cramped viewport, which the fade covers.
|
|
435
|
+
*/
|
|
436
|
+
const [flipped, setFlipped] = useState<boolean | null>(null);
|
|
417
437
|
/*
|
|
418
438
|
* The anchor is measured off the plain wrapper rather than off the animated
|
|
419
439
|
* field inside it. In `overlay` the wrapper *is* the field's box — the list
|
|
@@ -508,6 +528,12 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
508
528
|
|
|
509
529
|
const close = useCallback(() => setOpenState(false), [setOpenState]);
|
|
510
530
|
|
|
531
|
+
// A closed list has no side. Clearing the latch here rather than in `close`
|
|
532
|
+
// covers the caller closing it through `open` as well.
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
if (!open) setFlipped(null);
|
|
535
|
+
}, [open]);
|
|
536
|
+
|
|
511
537
|
// An open overlay list catches the Android back button, closing itself
|
|
512
538
|
// instead of popping the screen behind it.
|
|
513
539
|
useBackHandler(open && presentation === 'overlay', close);
|
|
@@ -741,6 +767,10 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
741
767
|
<Chip
|
|
742
768
|
key={entry}
|
|
743
769
|
size="sm"
|
|
770
|
+
// A chip is `self-start` by default, which overrides the row's
|
|
771
|
+
// `items-center` and leaves it riding high against the input.
|
|
772
|
+
// Inside a field it is one of several things sharing a line.
|
|
773
|
+
className="self-center"
|
|
744
774
|
onClose={disabled ? undefined : () => remove(entry)}
|
|
745
775
|
closeLabel={`Remove ${labelOf(entry)}`}
|
|
746
776
|
>
|
|
@@ -776,6 +806,10 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
776
806
|
}
|
|
777
807
|
}}
|
|
778
808
|
editable={!disabled}
|
|
809
|
+
// Android lays a single-line input's text against the top of its box
|
|
810
|
+
// unless told otherwise; iOS centres it. Without this the text sits
|
|
811
|
+
// above the chips on one platform and level with them on the other.
|
|
812
|
+
textAlignVertical="center"
|
|
779
813
|
placeholder={values.length && multiple ? undefined : placeholder}
|
|
780
814
|
placeholderTextColor={placeholderColor}
|
|
781
815
|
autoCapitalize="none"
|
|
@@ -840,12 +874,12 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
840
874
|
}
|
|
841
875
|
|
|
842
876
|
// Flip above the field when the list would run off the bottom — where the
|
|
843
|
-
// bottom is the top of the keyboard, not the bottom of the screen.
|
|
844
|
-
//
|
|
845
|
-
//
|
|
877
|
+
// bottom is the top of the keyboard, not the bottom of the screen. The list
|
|
878
|
+
// has no height until it has been laid out, so the side is decided on that
|
|
879
|
+
// first measurement and then held; see `flipped`.
|
|
846
880
|
const viewportBottom = screenHeight - keyboardHeight;
|
|
847
881
|
const spaceBelow = anchor ? viewportBottom - (anchor.y + anchor.height) - offset : 0;
|
|
848
|
-
const flip =
|
|
882
|
+
const flip = flipped ?? false;
|
|
849
883
|
|
|
850
884
|
const overlayPosition = anchor
|
|
851
885
|
? {
|
|
@@ -854,6 +888,11 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
854
888
|
...(flip
|
|
855
889
|
? { bottom: screenHeight - anchor.y + offset }
|
|
856
890
|
: { top: anchor.y + anchor.height + offset }),
|
|
891
|
+
// Above the dismiss strips it shares the portal with. They are earlier
|
|
892
|
+
// siblings so paint order already puts the list on top on iOS, but on
|
|
893
|
+
// Android a sibling without an elevation is not reliably ordered.
|
|
894
|
+
zIndex: 1,
|
|
895
|
+
elevation: 1,
|
|
857
896
|
...(contentWidth === 'field'
|
|
858
897
|
? { width: anchor.width }
|
|
859
898
|
: typeof contentWidth === 'number'
|
|
@@ -864,6 +903,21 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
864
903
|
}
|
|
865
904
|
: null;
|
|
866
905
|
|
|
906
|
+
/** The window minus the field: above it, below it, and either side of it. */
|
|
907
|
+
const catcherRects: ViewStyle[] = anchor
|
|
908
|
+
? [
|
|
909
|
+
{ top: 0, left: 0, right: 0, height: Math.max(anchor.y, 0) },
|
|
910
|
+
{ top: anchor.y + anchor.height, left: 0, right: 0, bottom: 0 },
|
|
911
|
+
{ top: anchor.y, height: anchor.height, left: 0, width: Math.max(anchor.x, 0) },
|
|
912
|
+
{
|
|
913
|
+
top: anchor.y,
|
|
914
|
+
height: anchor.height,
|
|
915
|
+
left: anchor.x + anchor.width,
|
|
916
|
+
right: 0,
|
|
917
|
+
},
|
|
918
|
+
]
|
|
919
|
+
: [];
|
|
920
|
+
|
|
867
921
|
return (
|
|
868
922
|
<ComboboxContext.Provider value={context}>
|
|
869
923
|
<View ref={fieldRef} className={cn(slots.root(), className)} {...props}>
|
|
@@ -879,26 +933,42 @@ function ComboboxRoot<Mode extends ComboboxMode = 'single'>({
|
|
|
879
933
|
* affordance for a pointer, and announcing it would put an unlabelled
|
|
880
934
|
* full-screen "button" ahead of the options in the reading order.
|
|
881
935
|
* Escaping the list is the back gesture's job.
|
|
936
|
+
*
|
|
937
|
+
* "Anywhere else" has to exclude the field, and one window-sized view
|
|
938
|
+
* cannot: covering the control that opened the list means a tap on
|
|
939
|
+
* the input to carry on typing, or on the chevron to close it, is
|
|
940
|
+
* spent dismissing instead and the field only answers on the second
|
|
941
|
+
* try. A view cannot have a hole cut in it, so the catcher is the
|
|
942
|
+
* four strips around the field's measured rect — which leaves the
|
|
943
|
+
* field uncovered and directly tappable.
|
|
882
944
|
*/}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
945
|
+
{catcherRects.map((rect, index) => (
|
|
946
|
+
<Pressable
|
|
947
|
+
key={index}
|
|
948
|
+
accessible={false}
|
|
949
|
+
importantForAccessibility="no-hide-descendants"
|
|
950
|
+
accessibilityElementsHidden
|
|
951
|
+
onPress={() => {
|
|
952
|
+
close();
|
|
953
|
+
inputRef.current?.blur();
|
|
954
|
+
}}
|
|
955
|
+
style={[{ position: 'absolute' }, rect]}
|
|
956
|
+
/>
|
|
957
|
+
))}
|
|
893
958
|
{/* Portalled out of this subtree — re-provide the context so
|
|
894
959
|
Combobox.Item keeps working. */}
|
|
895
960
|
<ComboboxContext.Provider value={context}>
|
|
896
961
|
<Animated.View
|
|
897
962
|
entering={FadeIn.duration(140)}
|
|
898
963
|
exiting={FadeOut.duration(120)}
|
|
899
|
-
onLayout={(event: LayoutChangeEvent) =>
|
|
900
|
-
|
|
901
|
-
|
|
964
|
+
onLayout={(event: LayoutChangeEvent) => {
|
|
965
|
+
// The first measurement decides the side, and nothing after it
|
|
966
|
+
// does — see `flipped`.
|
|
967
|
+
const measured = event.nativeEvent.layout.height;
|
|
968
|
+
setFlipped((current) =>
|
|
969
|
+
current === null ? measured > spaceBelow : current
|
|
970
|
+
);
|
|
971
|
+
}}
|
|
902
972
|
style={overlayPosition}
|
|
903
973
|
/*
|
|
904
974
|
* The floating list covers the screen with a catcher and takes
|
|
@@ -103,6 +103,7 @@ const DISMISS_VELOCITY = 650;
|
|
|
103
103
|
|
|
104
104
|
export type DrawerSide = 'start' | 'end' | 'top' | 'bottom';
|
|
105
105
|
export type DrawerSize = 'sm' | 'md' | 'lg' | 'full';
|
|
106
|
+
export type DrawerCloseSide = 'start' | 'end';
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* How much of the screen each size asks for, and the cap it is never allowed
|
|
@@ -168,7 +169,7 @@ function useDrawer(component: string): DrawerContextValue {
|
|
|
168
169
|
* so a header can leave it clear rather than wrap underneath it.
|
|
169
170
|
*/
|
|
170
171
|
interface DrawerSurfaceValue {
|
|
171
|
-
|
|
172
|
+
closeSide: DrawerCloseSide;
|
|
172
173
|
showClose: boolean;
|
|
173
174
|
}
|
|
174
175
|
|
|
@@ -270,6 +271,17 @@ export interface DrawerContentProps extends ViewProps {
|
|
|
270
271
|
swipeToDismiss?: boolean;
|
|
271
272
|
/** Show a close button in the drawer's inner top corner. Default true. */
|
|
272
273
|
showClose?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Which top corner the close button takes. Logical, like `side`: `end` is the
|
|
276
|
+
* corner text runs toward, so it is the right one in a left-to-right app and
|
|
277
|
+
* the left one in a right-to-left one.
|
|
278
|
+
*
|
|
279
|
+
* Defaults to the corner *away* from the docked edge, so an `end` drawer's
|
|
280
|
+
* button does not sit against the screen edge the panel came out of. Set it
|
|
281
|
+
* when the drawer reads better with the button on the outer corner instead —
|
|
282
|
+
* a panel people close by reaching for the same corner every time.
|
|
283
|
+
*/
|
|
284
|
+
closeSide?: DrawerCloseSide;
|
|
273
285
|
/**
|
|
274
286
|
* Frost the screen behind the drawer instead of dimming it. Needs the
|
|
275
287
|
* optional `expo-blur`; without it this dims, rather than failing.
|
|
@@ -285,6 +297,7 @@ function DrawerContent({
|
|
|
285
297
|
dismissible = true,
|
|
286
298
|
swipeToDismiss = true,
|
|
287
299
|
showClose = true,
|
|
300
|
+
closeSide,
|
|
288
301
|
blur = false,
|
|
289
302
|
children,
|
|
290
303
|
...props
|
|
@@ -384,9 +397,17 @@ function DrawerContent({
|
|
|
384
397
|
: { transform: [{ translateY: travel.value * outward }] }
|
|
385
398
|
);
|
|
386
399
|
|
|
400
|
+
/*
|
|
401
|
+
* Resolved once and shared, because the button's corner and the header's
|
|
402
|
+
* clearance are the same decision seen from two places. Letting each work it
|
|
403
|
+
* out from `side` is how they drift, and a header padded on the wrong side
|
|
404
|
+
* runs its title straight under the button.
|
|
405
|
+
*/
|
|
406
|
+
const closeCorner: DrawerCloseSide = closeSide ?? (side === 'end' ? 'start' : 'end');
|
|
407
|
+
|
|
387
408
|
const surface = useMemo<DrawerSurfaceValue>(
|
|
388
|
-
() => ({
|
|
389
|
-
[
|
|
409
|
+
() => ({ closeSide: closeCorner, showClose }),
|
|
410
|
+
[closeCorner, showClose]
|
|
390
411
|
);
|
|
391
412
|
|
|
392
413
|
if (!open) return null;
|
|
@@ -508,10 +529,11 @@ function DrawerContent({
|
|
|
508
529
|
* Taking the same number the padding does is what keeps it
|
|
509
530
|
* level with the first line of the header on every side.
|
|
510
531
|
*
|
|
511
|
-
* The corner follows the docked edge. An `end`
|
|
512
|
-
* edge is the trailing one, so its button moves
|
|
513
|
-
* leading side rather than sitting against the screen
|
|
514
|
-
* the drawer came out of
|
|
532
|
+
* The corner follows the docked edge by default. An `end`
|
|
533
|
+
* drawer's own edge is the trailing one, so its button moves
|
|
534
|
+
* to the leading side rather than sitting against the screen
|
|
535
|
+
* edge the drawer came out of — and `closeSide` overrides
|
|
536
|
+
* that for a panel that wants the same corner every time.
|
|
515
537
|
*/}
|
|
516
538
|
{showClose ? (
|
|
517
539
|
<Pressable
|
|
@@ -521,7 +543,7 @@ function DrawerContent({
|
|
|
521
543
|
hitSlop={8}
|
|
522
544
|
className={cn(
|
|
523
545
|
'absolute z-10 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70',
|
|
524
|
-
|
|
546
|
+
closeCorner === 'start' ? 'start-4' : 'end-4'
|
|
525
547
|
)}
|
|
526
548
|
style={{ top: padding.paddingTop }}
|
|
527
549
|
>
|
|
@@ -569,7 +591,7 @@ function DrawerHeader({
|
|
|
569
591
|
*/
|
|
570
592
|
const clearance = !surface?.showClose
|
|
571
593
|
? undefined
|
|
572
|
-
: surface.
|
|
594
|
+
: surface.closeSide === 'start'
|
|
573
595
|
? 'ps-14'
|
|
574
596
|
: 'pe-14';
|
|
575
597
|
|