@trackunit/react-map 0.2.172 → 0.2.174

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 (43) hide show
  1. package/index.cjs.js +165 -33
  2. package/index.esm.js +165 -33
  3. package/migrations/entry.js.map +1 -0
  4. package/package.json +9 -9
  5. package/src/appearance/appearanceTypes.d.ts +17 -7
  6. package/src/appearance/useMapAppearanceControls.d.ts +2 -0
  7. package/src/controls/renderers/boolean/MenuBooleanItem.d.ts +14 -1
  8. package/src/controls/types.d.ts +18 -0
  9. package/src/core/types.d.ts +13 -0
  10. package/translation.cjs.js +3 -1
  11. package/translation.esm.js +3 -1
  12. package/translation10.cjs.js +3 -1
  13. package/translation10.esm.js +3 -1
  14. package/translation11.cjs.js +3 -1
  15. package/translation11.esm.js +3 -1
  16. package/translation12.cjs.js +3 -1
  17. package/translation12.esm.js +3 -1
  18. package/translation13.cjs.js +3 -1
  19. package/translation13.esm.js +3 -1
  20. package/translation14.cjs.js +3 -1
  21. package/translation14.esm.js +3 -1
  22. package/translation15.cjs.js +3 -1
  23. package/translation15.esm.js +3 -1
  24. package/translation16.cjs.js +3 -1
  25. package/translation16.esm.js +3 -1
  26. package/translation17.cjs.js +3 -1
  27. package/translation17.esm.js +3 -1
  28. package/translation2.cjs.js +3 -1
  29. package/translation2.esm.js +3 -1
  30. package/translation3.cjs.js +3 -1
  31. package/translation3.esm.js +3 -1
  32. package/translation4.cjs.js +3 -1
  33. package/translation4.esm.js +3 -1
  34. package/translation5.cjs.js +3 -1
  35. package/translation5.esm.js +3 -1
  36. package/translation6.cjs.js +3 -1
  37. package/translation6.esm.js +3 -1
  38. package/translation7.cjs.js +3 -1
  39. package/translation7.esm.js +3 -1
  40. package/translation8.cjs.js +3 -1
  41. package/translation8.esm.js +3 -1
  42. package/translation9.cjs.js +3 -1
  43. package/translation9.esm.js +3 -1
package/index.esm.js CHANGED
@@ -39,7 +39,9 @@ var defaultTranslations = {
39
39
  "settings.mapStyle.roadmap": "Road map",
40
40
  "settings.mapStyle.roads": "Roads",
41
41
  "settings.mapStyle.satellite": "Satellite",
42
- "settings.mapStyle.title": "Map style"
42
+ "settings.mapStyle.title": "Map style",
43
+ "settings.mapStyle.traffic": "Traffic",
44
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
43
45
  };
44
46
 
45
47
  /** The translation namespace for this library */
@@ -1369,6 +1371,8 @@ const useMap = (adapterConfig) => {
1369
1371
  setMapType: adapter.setMapType.bind(adapter),
1370
1372
  setTheme: adapter.setTheme.bind(adapter),
1371
1373
  setShowRoads: adapter.setShowRoads.bind(adapter),
1374
+ // Optional capability: stays undefined for adapters without traffic support
1375
+ setShowTraffic: adapter.setShowTraffic?.bind(adapter),
1372
1376
  }), [adapter]);
1373
1377
  // Stable event subscription callback
1374
1378
  const on = useMemo(() => adapter.on.bind(adapter), [adapter]);
@@ -1845,9 +1849,12 @@ const BooleanButton = ({ config, overlaySide = undefined, resolved }) => {
1845
1849
  if (icon === undefined) {
1846
1850
  return null;
1847
1851
  }
1848
- const tooltipLabel = t("controls.boolean.tooltip", { label });
1852
+ // A disabled control's tooltip explains what unblocks it rather than what clicking would do
1853
+ const tooltipLabel = config.disabled === true && config.disabledReason !== undefined
1854
+ ? config.disabledReason
1855
+ : t("controls.boolean.tooltip", { label });
1849
1856
  const useRightOverlay = overlaySide === "right";
1850
- return (jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, children: jsx(IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: tooltipLabel }), "aria-pressed": value, className: className, "data-testid": dataTestId, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }));
1857
+ return (jsx(StandaloneControlTooltip, { label: tooltipLabel, overlaySide: overlaySide, children: jsx(IconButton, { ...(useRightOverlay ? { ariaLabel: label } : { title: tooltipLabel }), "aria-pressed": value, className: className, "data-testid": dataTestId, disabled: config.disabled ?? false, icon: jsx(Icon, { name: icon, size: "small" }), onClick: () => onClickChange(!value), size: size, variant: value ? "primary" : "secondary" }) }));
1851
1858
  };
1852
1859
 
1853
1860
  /**
@@ -2089,6 +2096,13 @@ const usePopoverPlacement = (triggerRef, containerRef, options = {}) => {
2089
2096
  return placement;
2090
2097
  };
2091
2098
 
2099
+ const cvaMenuBooleanItemLabel = cva({
2100
+ base: ["flex", "items-center", "gap-2", "text-sm", "font-medium"],
2101
+ variants: {
2102
+ disabled: { true: "text-neutral-400", false: "text-neutral-700" },
2103
+ },
2104
+ defaultVariants: { disabled: false },
2105
+ });
2092
2106
  /**
2093
2107
  * Shared menu layout for boolean controls (toggle and checkbox).
2094
2108
  * Renders: [label] [icon] ...space... [children]
@@ -2096,22 +2110,30 @@ const usePopoverPlacement = (triggerRef, containerRef, options = {}) => {
2096
2110
  * The caller provides the specific input control (ToggleSwitch or Checkbox)
2097
2111
  * as children.
2098
2112
  *
2113
+ * The disabled reason renders as a tooltip on the row itself, not on the
2114
+ * input: the full-width dimmed row is what users hover, and a natively
2115
+ * disabled input (checkbox case) suppresses its own hover events anyway.
2116
+ *
2099
2117
  * @internal
2100
2118
  */
2101
- const MenuBooleanItem = ({ children, className, "data-testid": dataTestId, icon = undefined, label, }) => (jsxs("label", { className: cvaControlMenuItemBase({
2102
- className: [
2103
- "flex",
2104
- "cursor-pointer",
2105
- "items-center",
2106
- "justify-between",
2107
- "gap-3",
2108
- "hover:bg-neutral-50",
2109
- "active:bg-neutral-100",
2110
- "transition-colors",
2111
- "duration-150",
2112
- className,
2113
- ],
2114
- }), "data-testid": dataTestId, children: [jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-neutral-700", children: [label, icon !== undefined ? jsx(Icon, { className: "text-neutral-500", name: icon, size: "small" }) : null] }), children] }));
2119
+ const MenuBooleanItem = ({ children, className, "data-testid": dataTestId, disabled = false, disabledReason = undefined, disabledReasonId = undefined, icon = undefined, label, }) => {
2120
+ const row = (jsxs("label", { className: cvaControlMenuItemBase({
2121
+ className: [
2122
+ "flex",
2123
+ "items-center",
2124
+ "justify-between",
2125
+ "gap-3",
2126
+ "transition-colors",
2127
+ "duration-150",
2128
+ ...(disabled ? ["cursor-not-allowed"] : ["cursor-pointer", "hover:bg-neutral-50", "active:bg-neutral-100"]),
2129
+ className,
2130
+ ],
2131
+ }), "data-testid": dataTestId, children: [jsxs("span", { className: cvaMenuBooleanItemLabel({ disabled }), children: [label, icon !== undefined ? jsx(Icon, { className: "text-neutral-500", name: icon, size: "small" }) : null] }), disabled && disabledReason !== undefined ? (jsx("span", { className: "sr-only", id: disabledReasonId, children: disabledReason })) : null, children] }));
2132
+ if (disabled && disabledReason !== undefined) {
2133
+ return jsx(Tooltip, { label: disabledReason, children: row });
2134
+ }
2135
+ return row;
2136
+ };
2115
2137
 
2116
2138
  /**
2117
2139
  * Renders a checkbox control within a menu context.
@@ -2119,7 +2141,13 @@ const MenuBooleanItem = ({ children, className, "data-testid": dataTestId, icon
2119
2141
  *
2120
2142
  * @internal
2121
2143
  */
2122
- const MenuCheckbox = ({ config, resolved }) => (jsx(MenuBooleanItem, { className: config.className, "data-testid": config["data-testid"], icon: resolved.icon, label: config.label, children: jsx(Checkbox, { checked: config.value, onChange: e => config.onChange(e.target.checked) }) }));
2144
+ const MenuCheckbox = ({ config, resolved }) => {
2145
+ // See MenuToggle: the row tooltip is hover-only, so the input also points
2146
+ // aria-describedby at a visually hidden copy of the reason.
2147
+ const disabledReasonId = useId();
2148
+ const isDisabledWithReason = config.disabled === true && config.disabledReason !== undefined;
2149
+ return (jsx(MenuBooleanItem, { className: config.className, "data-testid": config["data-testid"], disabled: config.disabled, disabledReason: config.disabledReason, disabledReasonId: disabledReasonId, icon: resolved.icon, label: config.label, children: jsx(Checkbox, { "aria-describedby": isDisabledWithReason ? disabledReasonId : undefined, checked: config.value, disabled: config.disabled ?? false, onChange: e => config.onChange(e.target.checked) }) }));
2150
+ };
2123
2151
 
2124
2152
  /**
2125
2153
  * Renders a toggle control within a menu context.
@@ -2127,7 +2155,18 @@ const MenuCheckbox = ({ config, resolved }) => (jsx(MenuBooleanItem, { className
2127
2155
  *
2128
2156
  * @internal
2129
2157
  */
2130
- const MenuToggle = ({ config, resolved }) => (jsx(MenuBooleanItem, { className: config.className, "data-testid": config["data-testid"], icon: resolved.icon, label: config.label, children: jsx(ToggleSwitch, { onChange: toggled => config.onChange(toggled), showInputFocus: false, size: "small", tabIndex: -1, toggled: config.value }) }));
2158
+ const MenuToggle = ({ config, resolved }) => {
2159
+ // The reason must also reach non-mouse users: the row tooltip is hover-only,
2160
+ // so the input additionally points aria-describedby at a visually hidden copy.
2161
+ const disabledReasonId = useId();
2162
+ const isDisabledWithReason = config.disabled === true && config.disabledReason !== undefined;
2163
+ return (jsx(MenuBooleanItem, { className: config.className, "data-testid": config["data-testid"], disabled: config.disabled, disabledReason: config.disabledReason, disabledReasonId: disabledReasonId, icon: resolved.icon, label: config.label, children: jsx(ToggleSwitch, { "aria-describedby": isDisabledWithReason ? disabledReasonId : undefined,
2164
+ // readOnly, not disabled: same dimmed visual and blocked interaction, but the
2165
+ // input stays focusable and is announced as dimmed instead of losing
2166
+ // focusability and being skipped in some AT browse modes (see the `disabled`
2167
+ // JSDoc on BooleanControlFields for the toggle/checkbox split).
2168
+ onChange: toggled => config.onChange(toggled), readOnly: config.disabled ?? false, showInputFocus: false, size: "small", tabIndex: -1, toggled: config.value }) }));
2169
+ };
2131
2170
 
2132
2171
  /**
2133
2172
  * Renders a button control within a menu context.
@@ -2774,19 +2813,26 @@ const AppearancePreviewGrid = ({ adapterConfig, ariaLabel, options, currentTheme
2774
2813
  // Version
2775
2814
  // ============================================================================
2776
2815
  /**
2777
- * Increment this number to invalidate stored appearance settings in localStorage
2778
- * and reset all users to defaults.
2816
+ * Increment this number when the persisted appearance shape changes, and add a
2817
+ * matching migration step in useMapAppearanceControls so stored settings are
2818
+ * upgraded instead of reset. (v2: added showTraffic.)
2779
2819
  */
2780
- const APPEARANCE_SETTINGS_VERSION = 1;
2820
+ const APPEARANCE_SETTINGS_VERSION = 2;
2781
2821
  // ============================================================================
2782
2822
  // Persisted Appearance Schema (extends core mapAppearanceSchema with version)
2783
2823
  // ============================================================================
2784
2824
  const persistedAppearanceSchema = mapAppearanceSchema.extend({
2825
+ // Required here (unlike the core schema, where it is optional for external
2826
+ // compatibility): the v2 migration guarantees stored data carries the key,
2827
+ // and useLocalStorageReducer needs an input = output schema.
2828
+ showTraffic: z.boolean(),
2785
2829
  version: z.literal(APPEARANCE_SETTINGS_VERSION),
2786
2830
  });
2787
2831
  /** Default persisted appearance when nothing is stored */
2788
2832
  const DEFAULT_PERSISTED_APPEARANCE = {
2789
2833
  ...DEFAULT_MAP_APPEARANCE,
2834
+ // The core field is optional (absent = false); persisted state requires it
2835
+ showTraffic: DEFAULT_MAP_APPEARANCE.showTraffic ?? false,
2790
2836
  version: APPEARANCE_SETTINGS_VERSION,
2791
2837
  };
2792
2838
  /** Reducer for persisted appearance state */
@@ -2798,6 +2844,8 @@ const appearanceReducer = (state, action) => {
2798
2844
  return { ...state, mapType: action.payload };
2799
2845
  case "setShowRoads":
2800
2846
  return { ...state, showRoads: action.payload };
2847
+ case "setShowTraffic":
2848
+ return { ...state, showTraffic: action.payload };
2801
2849
  default: {
2802
2850
  throw new Error(`${action} is not known`);
2803
2851
  }
@@ -2806,12 +2854,39 @@ const appearanceReducer = (state, action) => {
2806
2854
 
2807
2855
  const DEFAULT_THEMES = ["light", "dark"];
2808
2856
  const DEFAULT_MAP_TYPES = ["roadmap", "satellite"];
2809
- const APPEARANCE_STORAGE_VERSION = 1;
2857
+ /**
2858
+ * The v1 persisted shape, frozen as a literal. Deliberately NOT derived from
2859
+ * the live mapAppearanceSchema: deriving would make every future appearance
2860
+ * field retroactively required on v1-era data, failing the parse below and
2861
+ * resetting exactly the users the migration exists to carry over.
2862
+ */
2863
+ const appearanceV1Schema = z.object({
2864
+ theme: z.enum(["light", "dark"]),
2865
+ mapType: z.enum(["roadmap", "satellite", "hybrid"]),
2866
+ showRoads: z.boolean(),
2867
+ version: z.literal(1),
2868
+ });
2810
2869
  const appearanceMigrationV1 = {
2811
- version: APPEARANCE_STORAGE_VERSION,
2870
+ version: 1,
2871
+ migrate: data => {
2872
+ const result = appearanceV1Schema.safeParse(data);
2873
+ // Pass unrecognized data through: it may already be a newer shape
2874
+ // (stored without a version envelope), which the next step recognizes.
2875
+ return result.success ? result.data : data;
2876
+ },
2877
+ };
2878
+ /** v2 added the showTraffic preference; carry v1 settings over instead of resetting them */
2879
+ const appearanceMigrationV2 = {
2880
+ version: APPEARANCE_SETTINGS_VERSION,
2812
2881
  migrate: data => {
2813
- const result = persistedAppearanceSchema.safeParse(data);
2814
- return result.success ? result.data : DEFAULT_PERSISTED_APPEARANCE;
2882
+ const current = persistedAppearanceSchema.safeParse(data);
2883
+ if (current.success) {
2884
+ return current.data;
2885
+ }
2886
+ const v1 = appearanceV1Schema.safeParse(data);
2887
+ return v1.success
2888
+ ? { ...v1.data, showTraffic: false, version: APPEARANCE_SETTINGS_VERSION }
2889
+ : DEFAULT_PERSISTED_APPEARANCE;
2815
2890
  },
2816
2891
  };
2817
2892
  const themeToTranslationKey = (theme) => {
@@ -2885,6 +2960,8 @@ const buildAppearanceOptions = (themes, mapTypes, t) => {
2885
2960
  * Returns an array with a single `MenuControlConfig` that contains:
2886
2961
  * - A custom item with a grid of live tiny map previews
2887
2962
  * - A toggle item for roads overlay (when satellite/hybrid is selected)
2963
+ * - A toggle item for live traffic (when the adapter supports it; disabled on
2964
+ * satellite while roads are off, since traffic only renders on hybrid)
2888
2965
  *
2889
2966
  * @example
2890
2967
  * ```tsx
@@ -2905,19 +2982,35 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2905
2982
  const themes = appearanceOptions?.themes ?? DEFAULT_THEMES;
2906
2983
  const mapTypes = appearanceOptions?.mapTypes ?? DEFAULT_MAP_TYPES;
2907
2984
  const showRoadsToggle = appearanceOptions?.roads !== false;
2985
+ // Traffic is an optional adapter capability: no setShowTraffic action means no toggle
2986
+ const setShowTraffic = api.actions.setShowTraffic;
2987
+ // `traffic: false` opts the whole surface out of traffic, not just out of the toggle:
2988
+ // with a shared persistence key another surface may have stored showTraffic: true,
2989
+ // and rendering traffic here with no toggle to turn it off would trap the user.
2990
+ const trafficOptedOut = appearanceOptions?.traffic === false;
2991
+ const showTrafficToggle = !trafficOptedOut && setShowTraffic !== undefined;
2908
2992
  const [localState, dispatch] = useLocalStorageReducer({
2909
2993
  key: persistenceKey,
2910
2994
  defaultState: DEFAULT_PERSISTED_APPEARANCE,
2911
- migration: { version: APPEARANCE_STORAGE_VERSION, steps: [appearanceMigrationV1] },
2995
+ migration: { version: APPEARANCE_SETTINGS_VERSION, steps: [appearanceMigrationV1, appearanceMigrationV2] },
2912
2996
  reducer: appearanceReducer,
2913
2997
  schema: persistedAppearanceSchema,
2914
2998
  });
2915
- // Push localStorage state to the adapter when it changes
2999
+ // Push localStorage state to the adapter when it changes. Traffic is pushed as the
3000
+ // raw preference: the adapter itself suppresses rendering where the provider cannot
3001
+ // draw traffic (plain satellite, low zoom), so the preference survives roads/mapType
3002
+ // changes and is restored when they change back.
2916
3003
  useWatch({
2917
- value: { mapType: localState.mapType, showRoads: localState.showRoads, theme: localState.theme },
2918
- onChange: ({ mapType, showRoads, theme }) => {
3004
+ value: {
3005
+ mapType: localState.mapType,
3006
+ showRoads: localState.showRoads,
3007
+ showTraffic: trafficOptedOut ? false : localState.showTraffic,
3008
+ theme: localState.theme,
3009
+ },
3010
+ onChange: ({ mapType, showRoads, showTraffic, theme }) => {
2919
3011
  void api.actions.setMapType(mapType);
2920
3012
  void api.actions.setShowRoads(showRoads);
3013
+ void setShowTraffic?.(showTraffic);
2921
3014
  void api.actions.setTheme(theme);
2922
3015
  },
2923
3016
  immediate: true,
@@ -2938,6 +3031,14 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2938
3031
  if (appearance.showRoads !== localState.showRoads) {
2939
3032
  dispatch({ type: "setShowRoads", payload: appearance.showRoads });
2940
3033
  }
3034
+ // Only sync traffic back where the toggle is live. An unsupported adapter (and
3035
+ // an opted-out surface) reports showTraffic: false forever, and syncing that
3036
+ // back would wipe a preference stored by another map surface sharing the same
3037
+ // persistence key.
3038
+ const adapterShowTraffic = appearance.showTraffic ?? false;
3039
+ if (showTrafficToggle && adapterShowTraffic !== localState.showTraffic) {
3040
+ dispatch({ type: "setShowTraffic", payload: adapterShowTraffic });
3041
+ }
2941
3042
  },
2942
3043
  skip: !api.state.isReady,
2943
3044
  });
@@ -2948,6 +3049,9 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2948
3049
  const handleRoadsChange = useCallback((value) => {
2949
3050
  dispatch({ type: "setShowRoads", payload: value });
2950
3051
  }, [dispatch]);
3052
+ const handleTrafficChange = useCallback((value) => {
3053
+ dispatch({ type: "setShowTraffic", payload: value });
3054
+ }, [dispatch]);
2951
3055
  const previewOptions = useMemo(() => buildAppearanceOptions(themes, mapTypes, t), [themes, mapTypes, t]);
2952
3056
  const adapterConfig = api.adapterConfig;
2953
3057
  return useMemo(() => {
@@ -2966,8 +3070,12 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2966
3070
  },
2967
3071
  ];
2968
3072
  const isSatelliteOrHybrid = localState.mapType === "satellite" || localState.mapType === "hybrid";
2969
- if (showRoadsToggle && isSatelliteOrHybrid) {
2970
- menuItems.push({ type: "separator" }, {
3073
+ const includeRoadsToggle = showRoadsToggle && isSatelliteOrHybrid;
3074
+ if (includeRoadsToggle || showTrafficToggle) {
3075
+ menuItems.push({ type: "separator" });
3076
+ }
3077
+ if (includeRoadsToggle) {
3078
+ menuItems.push({
2971
3079
  type: "toggle",
2972
3080
  id: "roads-overlay",
2973
3081
  label: t("settings.mapStyle.roads"),
@@ -2976,6 +3084,25 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2976
3084
  "data-testid": "map-style-roads-toggle",
2977
3085
  });
2978
3086
  }
3087
+ if (showTrafficToggle) {
3088
+ // On satellite, traffic can only render on top of the roads overlay (Google
3089
+ // renders no traffic on plain SATELLITE); the toggle is disabled while roads
3090
+ // are off but keeps DISPLAYING the persisted preference — the setting is
3091
+ // still "on", it just cannot apply until roads return, and the tooltip says
3092
+ // so. Explicit "hybrid" always renders roads (and traffic), so the gate
3093
+ // applies to "satellite" only.
3094
+ const trafficBlockedByRoads = localState.mapType === "satellite" && !localState.showRoads;
3095
+ menuItems.push({
3096
+ type: "toggle",
3097
+ id: "traffic-overlay",
3098
+ label: t("settings.mapStyle.traffic"),
3099
+ value: localState.showTraffic,
3100
+ disabled: trafficBlockedByRoads,
3101
+ disabledReason: t("settings.mapStyle.trafficRequiresRoads"),
3102
+ onChange: handleTrafficChange,
3103
+ "data-testid": "map-style-traffic-toggle",
3104
+ });
3105
+ }
2979
3106
  return [
2980
3107
  {
2981
3108
  type: "menu",
@@ -2992,10 +3119,13 @@ const useMapAppearanceControls = ({ api, options: appearanceOptions, persistence
2992
3119
  localState.mapType,
2993
3120
  localState.theme,
2994
3121
  localState.showRoads,
3122
+ localState.showTraffic,
2995
3123
  handleAppearanceChange,
2996
3124
  handleRoadsChange,
3125
+ handleTrafficChange,
2997
3126
  previewOptions,
2998
3127
  showRoadsToggle,
3128
+ showTrafficToggle,
2999
3129
  ]);
3000
3130
  };
3001
3131
 
@@ -12585,7 +12715,7 @@ const INITIAL_STATE = {
12585
12715
  isIdle: true,
12586
12716
  isReady: true,
12587
12717
  initializationFailed: false,
12588
- appearance: { theme: "light", mapType: "roadmap", showRoads: false },
12718
+ appearance: { theme: "light", mapType: "roadmap", showRoads: false, showTraffic: false },
12589
12719
  tileSize: 256,
12590
12720
  };
12591
12721
  const noop = () => undefined;
@@ -12634,6 +12764,7 @@ const mockMapApi = (overrides) => {
12634
12764
  setMapType: resolvedVoid,
12635
12765
  setTheme: resolvedVoid,
12636
12766
  setShowRoads: resolvedVoid,
12767
+ setShowTraffic: resolvedVoid,
12637
12768
  ...(overrides?.actions ?? {}),
12638
12769
  };
12639
12770
  const adapterConfig = {
@@ -12660,6 +12791,7 @@ const mockMapApi = (overrides) => {
12660
12791
  setMapType: resolvedVoid,
12661
12792
  setTheme: resolvedVoid,
12662
12793
  setShowRoads: resolvedVoid,
12794
+ setShowTraffic: resolvedVoid,
12663
12795
  on: () => noopUnsubscribe,
12664
12796
  notifyInitializationFailed: noop,
12665
12797
  destroy: noop,
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/react/map/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@trackunit/react-map",
3
- "version": "0.2.172",
3
+ "version": "0.2.174",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=24.x"
8
8
  },
9
9
  "dependencies": {
10
- "@trackunit/react-components": "3.2.1",
11
- "@trackunit/css-class-variance-utilities": "2.0.20",
12
- "@trackunit/react-form-components": "2.7.12",
13
- "@trackunit/react-core-hooks": "1.22.5",
14
- "@trackunit/geo-json-utils": "1.15.53",
15
- "@trackunit/i18n-library-translation": "2.5.5",
10
+ "@trackunit/react-components": "3.2.2",
11
+ "@trackunit/css-class-variance-utilities": "2.0.21",
12
+ "@trackunit/react-form-components": "2.7.13",
13
+ "@trackunit/react-core-hooks": "1.22.6",
14
+ "@trackunit/geo-json-utils": "1.15.54",
15
+ "@trackunit/i18n-library-translation": "2.5.6",
16
16
  "react-minimal-pie-chart": "^8.4.0",
17
- "@trackunit/react-map-adapter-shared": "0.0.138",
18
- "@trackunit/react-map-color-utils": "0.0.113",
17
+ "@trackunit/react-map-adapter-shared": "0.0.139",
18
+ "@trackunit/react-map-color-utils": "0.0.114",
19
19
  "@trackunit/ui-design-tokens": "1.15.41",
20
20
  "@floating-ui/react": "^0.26.25",
21
21
  "es-toolkit": "^1.39.10",
@@ -2,25 +2,29 @@ import type { AdapterConfig } from "@trackunit/react-map-adapter-shared";
2
2
  import { z } from "zod";
3
3
  import { type MapActions, type MapStatus, type MapTheme, type MapType } from "../core/types";
4
4
  /**
5
- * Increment this number to invalidate stored appearance settings in localStorage
6
- * and reset all users to defaults.
5
+ * Increment this number when the persisted appearance shape changes, and add a
6
+ * matching migration step in useMapAppearanceControls so stored settings are
7
+ * upgraded instead of reset. (v2: added showTraffic.)
7
8
  */
8
- export declare const APPEARANCE_SETTINGS_VERSION = 1;
9
+ export declare const APPEARANCE_SETTINGS_VERSION = 2;
9
10
  export declare const persistedAppearanceSchema: z.ZodObject<{
10
11
  theme: z.ZodEnum<["light", "dark"]>;
11
12
  mapType: z.ZodEnum<["roadmap", "satellite", "hybrid"]>;
12
13
  showRoads: z.ZodBoolean;
13
14
  } & {
14
- version: z.ZodLiteral<1>;
15
+ showTraffic: z.ZodBoolean;
16
+ version: z.ZodLiteral<2>;
15
17
  }, "strip", z.ZodTypeAny, {
16
18
  mapType: "roadmap" | "satellite" | "hybrid";
17
19
  theme: "light" | "dark";
18
- version: 1;
20
+ showTraffic: boolean;
21
+ version: 2;
19
22
  showRoads: boolean;
20
23
  }, {
21
24
  mapType: "roadmap" | "satellite" | "hybrid";
22
25
  theme: "light" | "dark";
23
- version: 1;
26
+ showTraffic: boolean;
27
+ version: 2;
24
28
  showRoads: boolean;
25
29
  }>;
26
30
  /** Appearance state persisted to localStorage (MapAppearance + migration version) */
@@ -39,7 +43,11 @@ type SetShowRoadsAction = Readonly<{
39
43
  type: "setShowRoads";
40
44
  payload: boolean;
41
45
  }>;
42
- export type AppearanceAction = Readonly<SetThemeAction | SetMapTypeAction | SetShowRoadsAction>;
46
+ type SetShowTrafficAction = Readonly<{
47
+ type: "setShowTraffic";
48
+ payload: boolean;
49
+ }>;
50
+ export type AppearanceAction = Readonly<SetThemeAction | SetMapTypeAction | SetShowRoadsAction | SetShowTrafficAction>;
43
51
  /** Reducer for persisted appearance state */
44
52
  export declare const appearanceReducer: (state: PersistedAppearance, action: AppearanceAction) => PersistedAppearance;
45
53
  /** Configuration for which appearance options to show in the settings panel */
@@ -50,6 +58,8 @@ export type AppearanceOptions = Readonly<{
50
58
  mapTypes?: ReadonlyArray<MapType>;
51
59
  /** Whether to show the roads toggle when satellite/hybrid is selected. Default: true */
52
60
  roads?: boolean;
61
+ /** Whether to show the traffic toggle when the adapter supports traffic. Default: true */
62
+ traffic?: boolean;
53
63
  }>;
54
64
  /**
55
65
  * Narrowed API surface that useMapAppearanceControls actually needs.
@@ -11,6 +11,8 @@ import { type UseMapAppearanceControlsParams } from "./appearanceTypes";
11
11
  * Returns an array with a single `MenuControlConfig` that contains:
12
12
  * - A custom item with a grid of live tiny map previews
13
13
  * - A toggle item for roads overlay (when satellite/hybrid is selected)
14
+ * - A toggle item for live traffic (when the adapter supports it; disabled on
15
+ * satellite while roads are off, since traffic only renders on hybrid)
14
16
  *
15
17
  * @example
16
18
  * ```tsx
@@ -4,6 +4,15 @@ type MenuBooleanItemProps = Readonly<{
4
4
  children: ReactNode;
5
5
  className?: string;
6
6
  "data-testid"?: string;
7
+ disabled?: boolean;
8
+ /** Why the control is disabled; shown as a tooltip over the whole row */
9
+ disabledReason?: string;
10
+ /**
11
+ * Id for the visually hidden copy of the reason, so callers can point the
12
+ * input's aria-describedby at it (tooltips are hover-only: invisible to
13
+ * touch, keyboard, and screen readers).
14
+ */
15
+ disabledReasonId?: string;
7
16
  icon?: IconName;
8
17
  label: string;
9
18
  }>;
@@ -14,7 +23,11 @@ type MenuBooleanItemProps = Readonly<{
14
23
  * The caller provides the specific input control (ToggleSwitch or Checkbox)
15
24
  * as children.
16
25
  *
26
+ * The disabled reason renders as a tooltip on the row itself, not on the
27
+ * input: the full-width dimmed row is what users hover, and a natively
28
+ * disabled input (checkbox case) suppresses its own hover events anyway.
29
+ *
17
30
  * @internal
18
31
  */
19
- export declare const MenuBooleanItem: ({ children, className, "data-testid": dataTestId, icon, label, }: MenuBooleanItemProps) => import("react").JSX.Element;
32
+ export declare const MenuBooleanItem: ({ children, className, "data-testid": dataTestId, disabled, disabledReason, disabledReasonId, icon, label, }: MenuBooleanItemProps) => import("react").JSX.Element;
20
33
  export {};
@@ -87,6 +87,24 @@ type BooleanControlFields = Readonly<{
87
87
  label: string;
88
88
  value: boolean;
89
89
  onChange: (value: boolean) => void;
90
+ /**
91
+ * Disables interaction and dims the control while keeping it visible,
92
+ * for options that only apply in the current context (e.g. the traffic
93
+ * toggle on satellite while roads are off).
94
+ *
95
+ * Rendering differs deliberately per control: menu toggles map this to
96
+ * ToggleSwitch's `readOnly` (announced as dimmed, keeps its switch role;
97
+ * native `disabled` would drop focusability and get skipped in some AT
98
+ * browse modes), while checkboxes use native `disabled` because Checkbox
99
+ * forwards `readOnly` to an input type="checkbox", where it is inert and
100
+ * does not block label-dispatched activation.
101
+ */
102
+ disabled?: boolean;
103
+ /**
104
+ * Why the control is disabled, shown as a tooltip on the disabled control
105
+ * so users learn what unblocks it (e.g. "Turn on Roads to show traffic").
106
+ */
107
+ disabledReason?: string;
90
108
  /**
91
109
  * State-dependent icons shown when the control is on or off (standalone only).
92
110
  * Both must be provided together — this prevents partial-icon bugs where
@@ -79,6 +79,19 @@ export type MapActions = Readonly<{
79
79
  * @returns Promise that resolves when the change is applied
80
80
  */
81
81
  setShowRoads: (showRoads: boolean) => Promise<void>;
82
+ /**
83
+ * Set whether live traffic conditions are shown on the map.
84
+ *
85
+ * Undefined when the adapter does not support traffic rendering — UI such as
86
+ * the built-in map-style control hides its traffic toggle in that case.
87
+ * The value records the user's preference; the adapter may still suppress
88
+ * rendering where the provider cannot draw traffic (plain satellite) or
89
+ * below TRAFFIC_MIN_ZOOM.
90
+ *
91
+ * @param showTraffic - Whether traffic should be shown
92
+ * @returns Promise that resolves when the change is applied
93
+ */
94
+ setShowTraffic?: (showTraffic: boolean) => Promise<void>;
82
95
  }>;
83
96
  /**
84
97
  * The API object returned as the second element of the useMap tuple.
@@ -22,7 +22,9 @@ var translation = {
22
22
  "settings.mapStyle.roadmap": "Straßenkarte",
23
23
  "settings.mapStyle.roads": "Straßen",
24
24
  "settings.mapStyle.satellite": "Satellit",
25
- "settings.mapStyle.title": "Kartenstil"
25
+ "settings.mapStyle.title": "Kartenstil",
26
+ "settings.mapStyle.traffic": "Traffic",
27
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
26
28
  };
27
29
 
28
30
  exports.default = translation;
@@ -20,7 +20,9 @@ var translation = {
20
20
  "settings.mapStyle.roadmap": "Straßenkarte",
21
21
  "settings.mapStyle.roads": "Straßen",
22
22
  "settings.mapStyle.satellite": "Satellit",
23
- "settings.mapStyle.title": "Kartenstil"
23
+ "settings.mapStyle.title": "Kartenstil",
24
+ "settings.mapStyle.traffic": "Traffic",
25
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
24
26
  };
25
27
 
26
28
  export { translation as default };
@@ -22,7 +22,9 @@ var translation = {
22
22
  "settings.mapStyle.roadmap": "Mapa drogowa",
23
23
  "settings.mapStyle.roads": "Drogi",
24
24
  "settings.mapStyle.satellite": "Satelita",
25
- "settings.mapStyle.title": "Styl mapy"
25
+ "settings.mapStyle.title": "Styl mapy",
26
+ "settings.mapStyle.traffic": "Traffic",
27
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
26
28
  };
27
29
 
28
30
  exports.default = translation;
@@ -20,7 +20,9 @@ var translation = {
20
20
  "settings.mapStyle.roadmap": "Mapa drogowa",
21
21
  "settings.mapStyle.roads": "Drogi",
22
22
  "settings.mapStyle.satellite": "Satelita",
23
- "settings.mapStyle.title": "Styl mapy"
23
+ "settings.mapStyle.title": "Styl mapy",
24
+ "settings.mapStyle.traffic": "Traffic",
25
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
24
26
  };
25
27
 
26
28
  export { translation as default };
@@ -22,7 +22,9 @@ var translation = {
22
22
  "settings.mapStyle.roadmap": "Mapa de estrada",
23
23
  "settings.mapStyle.roads": "Estradas",
24
24
  "settings.mapStyle.satellite": "Satélite",
25
- "settings.mapStyle.title": "Estilo do mapa"
25
+ "settings.mapStyle.title": "Estilo do mapa",
26
+ "settings.mapStyle.traffic": "Traffic",
27
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
26
28
  };
27
29
 
28
30
  exports.default = translation;
@@ -20,7 +20,9 @@ var translation = {
20
20
  "settings.mapStyle.roadmap": "Mapa de estrada",
21
21
  "settings.mapStyle.roads": "Estradas",
22
22
  "settings.mapStyle.satellite": "Satélite",
23
- "settings.mapStyle.title": "Estilo do mapa"
23
+ "settings.mapStyle.title": "Estilo do mapa",
24
+ "settings.mapStyle.traffic": "Traffic",
25
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
24
26
  };
25
27
 
26
28
  export { translation as default };
@@ -22,7 +22,9 @@ var translation = {
22
22
  "settings.mapStyle.roadmap": "Карта маршрута",
23
23
  "settings.mapStyle.roads": "Дороги",
24
24
  "settings.mapStyle.satellite": "Спутник",
25
- "settings.mapStyle.title": "Стиль карты"
25
+ "settings.mapStyle.title": "Стиль карты",
26
+ "settings.mapStyle.traffic": "Traffic",
27
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
26
28
  };
27
29
 
28
30
  exports.default = translation;
@@ -20,7 +20,9 @@ var translation = {
20
20
  "settings.mapStyle.roadmap": "Карта маршрута",
21
21
  "settings.mapStyle.roads": "Дороги",
22
22
  "settings.mapStyle.satellite": "Спутник",
23
- "settings.mapStyle.title": "Стиль карты"
23
+ "settings.mapStyle.title": "Стиль карты",
24
+ "settings.mapStyle.traffic": "Traffic",
25
+ "settings.mapStyle.trafficRequiresRoads": "Turn on Roads to show traffic"
24
26
  };
25
27
 
26
28
  export { translation as default };