panelui-native 0.103.2 → 0.104.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.
@@ -46,7 +46,14 @@ import {
46
46
  import { Pressable, StyleSheet, View, type PressableProps, type ViewProps } from 'react-native';
47
47
  import { useCSSVariable } from 'uniwind';
48
48
  import { tv } from 'tailwind-variants';
49
- import { CompassIcon, CrosshairIcon, MinusIcon, PlusIcon } from '../../icons';
49
+ import {
50
+ CompassIcon,
51
+ CrosshairIcon,
52
+ IconColorProvider,
53
+ MaximizeIcon,
54
+ MinusIcon,
55
+ PlusIcon,
56
+ } from '../../icons';
50
57
  import { Text, textChildren } from '../../primitives/text';
51
58
  import { cn } from '../../utils/cn';
52
59
  import {
@@ -64,6 +71,7 @@ import {
64
71
  type MapRef,
65
72
  type PixelPoint,
66
73
  type StyleSpecification,
74
+ type ViewPadding,
67
75
  type ViewState,
68
76
  } from './maplibre';
69
77
  import {
@@ -74,7 +82,15 @@ import {
74
82
  import { asMapLayer, partitionMapChildren } from './map-children';
75
83
 
76
84
  export { hasMapLibre, CARTO_SOURCE };
77
- export type { BasemapSource, BasemapTokens, LngLat, LngLatBounds, PixelPoint, ViewState };
85
+ export type {
86
+ BasemapSource,
87
+ BasemapTokens,
88
+ LngLat,
89
+ LngLatBounds,
90
+ PixelPoint,
91
+ ViewPadding,
92
+ ViewState,
93
+ };
78
94
  export type { MapFeatureAccessibility } from './map-accessibility';
79
95
 
80
96
  const mapVariants = tv({
@@ -179,6 +195,8 @@ interface MapContextValue {
179
195
  cameraRef: React.RefObject<CameraRef | null>;
180
196
  /** The style has loaded and the map is drawing. */
181
197
  ready: boolean;
198
+ /** Move the camera back to where the map opened. */
199
+ recenter(duration?: number): void;
182
200
  }
183
201
 
184
202
  interface AccessibleFeatureGroup {
@@ -207,15 +225,41 @@ export function useMap(): MapContextValue {
207
225
  /** Set by `Map.Marker`, so a popup inside one knows where it is. */
208
226
  const MarkerContext = createContext<LngLat | null>(null);
209
227
 
228
+ /** How `fitBounds` frames its box. */
229
+ export interface MapFitBoundsOptions {
230
+ /**
231
+ * Points kept clear around the box. One number for every side, or per side
232
+ * — for a sheet or a toolbar covering part of the map. Defaults to 48.
233
+ */
234
+ padding?: number | ViewPadding;
235
+ /** Animation length in milliseconds. 0 jumps. Defaults to 900. */
236
+ duration?: number;
237
+ }
238
+
210
239
  export interface MapHandle {
211
240
  /** Move the camera, animating unless `duration` is 0. */
212
241
  flyTo(options: { center: LngLat; zoom?: number; duration?: number }): void;
213
- /** Frame a bounding box, leaving `padding` points around it. */
214
- fitBounds(bounds: LngLatBounds, padding?: number): void;
242
+ /**
243
+ * Frame a bounding box, `[west, south, east, north]`. The second argument is
244
+ * either the padding in points or a `MapFitBoundsOptions`.
245
+ */
246
+ fitBounds(bounds: LngLatBounds, options?: number | MapFitBoundsOptions): void;
247
+ /**
248
+ * Return to the opening view — the `bounds`, or `center` and `zoom`, the map
249
+ * was mounted with. Animates unless `duration` is 0.
250
+ */
251
+ recenter(duration?: number): void;
215
252
  /** Where the map is looking right now. */
216
253
  getViewState(): Promise<ViewState | null>;
217
254
  }
218
255
 
256
+ /** Expands a padding shorthand to the four sides the camera takes. */
257
+ function toViewPadding(padding: number | ViewPadding): ViewPadding {
258
+ return typeof padding === 'number'
259
+ ? { top: padding, right: padding, bottom: padding, left: padding }
260
+ : padding;
261
+ }
262
+
219
263
  export interface MapProps extends Omit<ViewProps, 'children'> {
220
264
  children?: ReactNode;
221
265
  /** Initial centre, `[longitude, latitude]`. */
@@ -361,29 +405,52 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
361
405
  ]
362
406
  );
363
407
 
408
+ // The opening view, kept from the first render. The props describe where
409
+ // the map starts, not where it is, so a later change to them must not move
410
+ // the target `recenter` returns to without the caller asking.
411
+ const initialView = useRef({ center, zoom, bearing, pitch, bounds }).current;
412
+
413
+ const recenter = useCallback(
414
+ (duration = 600) => {
415
+ const camera = cameraRef.current;
416
+ if (!camera) return;
417
+ if (initialView.bounds) {
418
+ camera.fitBounds(initialView.bounds, { duration });
419
+ return;
420
+ }
421
+ camera.flyTo({
422
+ center: initialView.center ?? [0, 20],
423
+ zoom: initialView.zoom,
424
+ bearing: initialView.bearing ?? 0,
425
+ pitch: initialView.pitch ?? 0,
426
+ duration,
427
+ });
428
+ },
429
+ [initialView]
430
+ );
431
+
364
432
  useImperativeHandle(
365
433
  ref,
366
434
  () => ({
367
435
  flyTo: ({ center: to, zoom: z, duration = 900 }) =>
368
436
  cameraRef.current?.flyTo({ center: to, zoom: z, duration }),
369
- fitBounds: (box, padding = 48) =>
437
+ fitBounds: (box, options) => {
438
+ const { padding = 48, duration = 900 } =
439
+ typeof options === 'number' ? { padding: options } : (options ?? {});
370
440
  cameraRef.current?.fitBounds(box, {
371
- padding: {
372
- top: padding,
373
- right: padding,
374
- bottom: padding,
375
- left: padding,
376
- },
377
- duration: 900,
378
- }),
441
+ padding: toViewPadding(padding),
442
+ duration,
443
+ });
444
+ },
445
+ recenter,
379
446
  getViewState: async () => (await mapRef.current?.getViewState()) ?? null,
380
447
  }),
381
- []
448
+ [recenter]
382
449
  );
383
450
 
384
451
  const context = useMemo(
385
- () => ({ mapRef, cameraRef, ready, registerAccessibleFeatures }),
386
- [ready, registerAccessibleFeatures]
452
+ () => ({ mapRef, cameraRef, ready, recenter, registerAccessibleFeatures }),
453
+ [ready, recenter, registerAccessibleFeatures]
387
454
  );
388
455
 
389
456
  if (!hasMapLibre || !MapLibre) {
@@ -722,6 +789,11 @@ export interface MapControlsProps {
722
789
  locate?: boolean;
723
790
  /** Reset bearing and pitch to north and flat. */
724
791
  compass?: boolean;
792
+ /**
793
+ * Return to the view the map opened on — its `bounds`, or its `center` and
794
+ * `zoom`. The same move as `MapHandle.recenter()`.
795
+ */
796
+ recenter?: boolean;
725
797
  className?: string;
726
798
  /** Called with the located coordinate, so a caller can react to it. */
727
799
  onLocate?: (lngLat: LngLat) => void;
@@ -762,11 +834,16 @@ function MapControls({
762
834
  zoom = true,
763
835
  locate = false,
764
836
  compass = false,
837
+ recenter = false,
765
838
  className,
766
839
  onLocate,
767
840
  }: MapControlsProps) {
768
841
  const slots = mapVariants({ position });
769
- const { mapRef, cameraRef } = useMap();
842
+ const { mapRef, cameraRef, recenter: returnToStart } = useMap();
843
+ // Every glyph takes the theme's icon colour from here rather than from its
844
+ // own default, which was chosen for wherever that icon first appeared — the
845
+ // minus sign's is white, for a checked checkbox, and vanished on the card.
846
+ const iconTint = useToken('--color-muted-foreground', '#737373');
770
847
 
771
848
  // Read the live zoom rather than tracking it in state: the map is moved by
772
849
  // gestures too, and a counter kept alongside it drifts on the first pinch.
@@ -816,33 +893,42 @@ function MapControls({
816
893
  }, [mapRef, cameraRef]);
817
894
 
818
895
  return (
819
- <View className={slots.controls({ className })}>
820
- {zoom ? (
821
- <View className={slots.group()}>
822
- <ControlButton label="Zoom in" onPress={() => nudgeZoom(1)}>
823
- <PlusIcon size={16} />
824
- </ControlButton>
825
- <View className="h-px bg-border" />
826
- <ControlButton label="Zoom out" onPress={() => nudgeZoom(-1)}>
827
- <MinusIcon size={16} />
828
- </ControlButton>
829
- </View>
830
- ) : null}
831
- {compass ? (
832
- <View className={slots.group()}>
833
- <ControlButton label="Face north" onPress={resetNorth}>
834
- <CompassIcon size={16} />
835
- </ControlButton>
836
- </View>
837
- ) : null}
838
- {locate ? (
839
- <View className={slots.group()}>
840
- <ControlButton label="Show my location" onPress={handleLocate}>
841
- <CrosshairIcon size={16} />
842
- </ControlButton>
843
- </View>
844
- ) : null}
845
- </View>
896
+ <IconColorProvider color={iconTint}>
897
+ <View className={slots.controls({ className })}>
898
+ {zoom ? (
899
+ <View className={slots.group()}>
900
+ <ControlButton label="Zoom in" onPress={() => nudgeZoom(1)}>
901
+ <PlusIcon size={16} />
902
+ </ControlButton>
903
+ <View className="h-px bg-border" />
904
+ <ControlButton label="Zoom out" onPress={() => nudgeZoom(-1)}>
905
+ <MinusIcon size={16} strokeWidth={2} />
906
+ </ControlButton>
907
+ </View>
908
+ ) : null}
909
+ {recenter ? (
910
+ <View className={slots.group()}>
911
+ <ControlButton label="Recenter map" onPress={() => returnToStart()}>
912
+ <MaximizeIcon size={16} />
913
+ </ControlButton>
914
+ </View>
915
+ ) : null}
916
+ {compass ? (
917
+ <View className={slots.group()}>
918
+ <ControlButton label="Face north" onPress={resetNorth}>
919
+ <CompassIcon size={16} />
920
+ </ControlButton>
921
+ </View>
922
+ ) : null}
923
+ {locate ? (
924
+ <View className={slots.group()}>
925
+ <ControlButton label="Show my location" onPress={handleLocate}>
926
+ <CrosshairIcon size={16} />
927
+ </ControlButton>
928
+ </View>
929
+ ) : null}
930
+ </View>
931
+ </IconColorProvider>
846
932
  );
847
933
  }
848
934
  MapControls.displayName = 'Map.Controls';
package/src/index.ts CHANGED
@@ -340,6 +340,7 @@ export {
340
340
  CARTO_SOURCE,
341
341
  type MapProps,
342
342
  type MapHandle,
343
+ type MapFitBoundsOptions,
343
344
  type MapMarkerProps,
344
345
  type MapLabelProps,
345
346
  type MapPopupProps,
@@ -356,6 +357,7 @@ export {
356
357
  type BasemapTokens,
357
358
  type LngLat,
358
359
  type LngLatBounds,
360
+ type ViewPadding,
359
361
  type ViewState,
360
362
  } from './components/map';
361
363
  export {
@@ -870,6 +872,14 @@ export {
870
872
  type KpiGoodDirection,
871
873
  type KpiTone,
872
874
  } from './components/kpi';
875
+ export {
876
+ Leaderboard,
877
+ rankEntries,
878
+ type LeaderboardEntry,
879
+ type LeaderboardPodium,
880
+ type LeaderboardProps,
881
+ type LeaderboardRanked,
882
+ } from './components/leaderboard';
873
883
  export { Label, type LabelProps, type LabelTextProps } from './components/label';
874
884
  export {
875
885
  LineChart,