@carto/ps-react-ui 4.12.2 → 4.13.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.
Files changed (98) hide show
  1. package/dist/components.js +706 -658
  2. package/dist/components.js.map +1 -1
  3. package/dist/legend/stores.js +20 -0
  4. package/dist/legend/stores.js.map +1 -0
  5. package/dist/legend.js +1536 -0
  6. package/dist/legend.js.map +1 -0
  7. package/dist/selectors-DwHj6mE2.js +388 -0
  8. package/dist/selectors-DwHj6mE2.js.map +1 -0
  9. package/dist/types/components/index.d.ts +1 -0
  10. package/dist/types/components/responsive-drawer/responsive-drawer.d.ts +35 -3
  11. package/dist/types/legend/components/contexts.d.ts +20 -0
  12. package/dist/types/legend/components/icons.d.ts +12 -0
  13. package/dist/types/legend/components/index.d.ts +26 -0
  14. package/dist/types/legend/components/legend-actions/legend-actions.d.ts +24 -0
  15. package/dist/types/legend/components/legend-actions/styles.d.ts +7 -0
  16. package/dist/types/legend/components/legend-category/legend-category-ui.d.ts +13 -0
  17. package/dist/types/legend/components/legend-category/styles.d.ts +52 -0
  18. package/dist/types/legend/components/legend-config-select/legend-config-select.d.ts +14 -0
  19. package/dist/types/legend/components/legend-config-select/styles.d.ts +44 -0
  20. package/dist/types/legend/components/legend-group/legend-group.d.ts +21 -0
  21. package/dist/types/legend/components/legend-group/styles.d.ts +63 -0
  22. package/dist/types/legend/components/legend-icon/legend-icon-ui.d.ts +10 -0
  23. package/dist/types/legend/components/legend-icon/styles.d.ts +52 -0
  24. package/dist/types/legend/components/legend-item/legend-item-ui.d.ts +14 -0
  25. package/dist/types/legend/components/legend-item/legend-item.d.ts +10 -0
  26. package/dist/types/legend/components/legend-item/styles.d.ts +7 -0
  27. package/dist/types/legend/components/legend-opacity/legend-opacity.d.ts +32 -0
  28. package/dist/types/legend/components/legend-opacity/styles.d.ts +23 -0
  29. package/dist/types/legend/components/legend-proportion/legend-proportion-ui.d.ts +16 -0
  30. package/dist/types/legend/components/legend-proportion/styles.d.ts +37 -0
  31. package/dist/types/legend/components/legend-ramp/legend-ramp-ui.d.ts +20 -0
  32. package/dist/types/legend/components/legend-ramp/styles.d.ts +53 -0
  33. package/dist/types/legend/components/legend-row/legend-row.d.ts +23 -0
  34. package/dist/types/legend/components/legend-row/styles.d.ts +77 -0
  35. package/dist/types/legend/components/legend-row-menu/legend-row-menu.d.ts +18 -0
  36. package/dist/types/legend/components/legend-row-menu/legend-zoom-to.d.ts +15 -0
  37. package/dist/types/legend/components/legend-swatch/legend-swatch.d.ts +26 -0
  38. package/dist/types/legend/components/legend-visibility-toggle/legend-visibility-toggle.d.ts +35 -0
  39. package/dist/types/legend/index.d.ts +88 -0
  40. package/dist/types/legend/provider/index.d.ts +6 -0
  41. package/dist/types/legend/provider/labels.d.ts +27 -0
  42. package/dist/types/legend/provider/legend-config-context.d.ts +7 -0
  43. package/dist/types/legend/provider/legend-provider.d.ts +38 -0
  44. package/dist/types/legend/stores/index.d.ts +5 -0
  45. package/dist/types/legend/stores/legend-context.d.ts +4 -0
  46. package/dist/types/legend/stores/legend-store-registry.d.ts +39 -0
  47. package/dist/types/legend/stores/legend-store.test.d.ts +1 -0
  48. package/dist/types/legend/stores/selectors.d.ts +33 -0
  49. package/dist/types/legend/stores/types.d.ts +275 -0
  50. package/package.json +11 -3
  51. package/src/components/geolocation-controls/geolocation-controls.test.tsx +15 -0
  52. package/src/components/geolocation-controls/geolocation-controls.tsx +19 -3
  53. package/src/components/index.ts +2 -0
  54. package/src/components/responsive-drawer/responsive-drawer.test.tsx +129 -25
  55. package/src/components/responsive-drawer/responsive-drawer.tsx +149 -16
  56. package/src/legend/components/contexts.ts +32 -0
  57. package/src/legend/components/icons.tsx +22 -0
  58. package/src/legend/components/index.ts +39 -0
  59. package/src/legend/components/legend-actions/legend-actions.tsx +57 -0
  60. package/src/legend/components/legend-actions/styles.ts +9 -0
  61. package/src/legend/components/legend-category/legend-category-ui.tsx +91 -0
  62. package/src/legend/components/legend-category/styles.ts +61 -0
  63. package/src/legend/components/legend-config-select/legend-config-select.tsx +96 -0
  64. package/src/legend/components/legend-config-select/styles.ts +48 -0
  65. package/src/legend/components/legend-group/legend-group.test.tsx +115 -0
  66. package/src/legend/components/legend-group/legend-group.tsx +88 -0
  67. package/src/legend/components/legend-group/styles.ts +82 -0
  68. package/src/legend/components/legend-icon/legend-icon-ui.tsx +56 -0
  69. package/src/legend/components/legend-icon/styles.ts +56 -0
  70. package/src/legend/components/legend-item/legend-item-ui.test.tsx +362 -0
  71. package/src/legend/components/legend-item/legend-item-ui.tsx +43 -0
  72. package/src/legend/components/legend-item/legend-item.tsx +34 -0
  73. package/src/legend/components/legend-item/styles.ts +11 -0
  74. package/src/legend/components/legend-opacity/legend-opacity.tsx +179 -0
  75. package/src/legend/components/legend-opacity/styles.ts +28 -0
  76. package/src/legend/components/legend-proportion/legend-proportion-ui.tsx +122 -0
  77. package/src/legend/components/legend-proportion/styles.ts +43 -0
  78. package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +159 -0
  79. package/src/legend/components/legend-ramp/styles.ts +62 -0
  80. package/src/legend/components/legend-row/legend-row.test.tsx +498 -0
  81. package/src/legend/components/legend-row/legend-row.tsx +129 -0
  82. package/src/legend/components/legend-row/styles.ts +111 -0
  83. package/src/legend/components/legend-row-menu/legend-row-menu.tsx +68 -0
  84. package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +35 -0
  85. package/src/legend/components/legend-swatch/legend-swatch.test.tsx +109 -0
  86. package/src/legend/components/legend-swatch/legend-swatch.tsx +237 -0
  87. package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +127 -0
  88. package/src/legend/index.ts +121 -0
  89. package/src/legend/provider/index.ts +6 -0
  90. package/src/legend/provider/labels.ts +59 -0
  91. package/src/legend/provider/legend-config-context.ts +15 -0
  92. package/src/legend/provider/legend-provider.tsx +102 -0
  93. package/src/legend/stores/index.ts +46 -0
  94. package/src/legend/stores/legend-context.ts +17 -0
  95. package/src/legend/stores/legend-store-registry.ts +639 -0
  96. package/src/legend/stores/legend-store.test.ts +457 -0
  97. package/src/legend/stores/selectors.ts +65 -0
  98. package/src/legend/stores/types.ts +347 -0
@@ -0,0 +1,35 @@
1
+ import { LegendVisibility } from '../../stores';
2
+ import { LegendLabels } from '../../provider/labels';
3
+ export type LegendVisibilityToggleProps = {
4
+ /**
5
+ * Override the aria labels (defaults: the layer pair in rows and controlled
6
+ * mode, the group pair in group headers) — e.g.
7
+ * `labels={{ hideLayer: 'Hide section', showLayer: 'Show section' }}`.
8
+ */
9
+ labels?: Pick<LegendLabels, 'hideLayer' | 'showLayer'>;
10
+ } & ({
11
+ /**
12
+ * Controlled mode: current state — renders the matching eye (`mixed` →
13
+ * the indeterminate icon). Use it for element kinds beyond layer/group.
14
+ */
15
+ visibility: LegendVisibility;
16
+ /**
17
+ * Controlled mode: called with the **next** visibility (`hidden` →
18
+ * `true`; `visible`/`mixed` → `false`) — mirrors the store setters.
19
+ */
20
+ onToggle: (visible: boolean) => void;
21
+ } | {
22
+ visibility?: never;
23
+ onToggle?: never;
24
+ });
25
+ /**
26
+ * Agnostic visibility eye. Pass `visibility` + `onToggle` to control it for
27
+ * any element kind (no context needed); without them it wires itself to the
28
+ * nearest context — the layer eye inside a `Legend.Row`, the tri-state group
29
+ * eye (cascading to every member) inside a `Legend.Group`. Pinned `.active`
30
+ * in the hover-fade group while not fully visible. Renders nothing without
31
+ * props or context (dev-only warning).
32
+ *
33
+ * @experimental This API is new and may change in a future release.
34
+ */
35
+ export declare function LegendVisibilityToggle({ visibility, onToggle, labels, }: LegendVisibilityToggleProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,88 @@
1
+ import { LegendProvider } from './provider/legend-provider';
2
+ import { LegendGroup } from './components/legend-group/legend-group';
3
+ import { LegendRow } from './components/legend-row/legend-row';
4
+ import { LegendActions } from './components/legend-actions/legend-actions';
5
+ import { LegendVisibilityToggle } from './components/legend-visibility-toggle/legend-visibility-toggle';
6
+ import { LegendRowMenu } from './components/legend-row-menu/legend-row-menu';
7
+ import { LegendZoomTo } from './components/legend-row-menu/legend-zoom-to';
8
+ import { LegendConfigSelect } from './components/legend-config-select/legend-config-select';
9
+ import { LegendItem } from './components/legend-item/legend-item';
10
+ import { LegendItemUI } from './components/legend-item/legend-item-ui';
11
+ import { LegendSwatch } from './components/legend-swatch/legend-swatch';
12
+ import { LegendCategoryUI } from './components/legend-category/legend-category-ui';
13
+ import { LegendRampUI } from './components/legend-ramp/legend-ramp-ui';
14
+ import { LegendProportionUI } from './components/legend-proportion/legend-proportion-ui';
15
+ import { LegendIconUI } from './components/legend-icon/legend-icon-ui';
16
+ /**
17
+ * Curated namespace export — the entry point to the legend module.
18
+ *
19
+ * Composable, widgets-v2 style: the lib ships wrapper shells + standalone
20
+ * store-connected actions, and the **app mounts the legend** in its own
21
+ * chrome (panel/drawer/sidebar). `Legend.Row`/`Legend.Group` partition their
22
+ * children — `<Legend.Actions>` renders in the sticky header (hover-fade),
23
+ * everything else in the collapsible body.
24
+ *
25
+ * ```tsx
26
+ * import { Legend } from '@carto/ps-react-ui/legend'
27
+ *
28
+ * <Legend.Provider id='map-legend' layers={layers} groups={groups}>
29
+ * <Legend.Group groupId='demographics'>
30
+ * <Legend.Actions>
31
+ * <Legend.VisibilityToggle /> // tri-state group eye
32
+ * </Legend.Actions>
33
+ * <Legend.Row layerId='income'>
34
+ * // Actions read L→R: options · opacity · visibility
35
+ * <Legend.Actions>
36
+ * <Legend.RowMenu>
37
+ * <Legend.ZoomTo onZoomTo={zoomTo} />
38
+ * </Legend.RowMenu>
39
+ * <Legend.Opacity.Trigger />
40
+ * <Legend.VisibilityToggle /> // layer eye
41
+ * </Legend.Actions>
42
+ * <Legend.ConfigSelect />
43
+ * <Legend.Item />
44
+ * <Legend.Opacity.Popover /> // anchors to the row header
45
+ * </Legend.Row>
46
+ * </Legend.Group>
47
+ * </Legend.Provider>
48
+ * ```
49
+ *
50
+ * @experimental This API is new and may change in a future release.
51
+ */
52
+ export declare const Legend: {
53
+ readonly Provider: typeof LegendProvider;
54
+ readonly Group: typeof LegendGroup;
55
+ readonly Row: typeof LegendRow;
56
+ readonly Actions: typeof LegendActions;
57
+ readonly VisibilityToggle: typeof LegendVisibilityToggle;
58
+ readonly RowMenu: typeof LegendRowMenu;
59
+ readonly ZoomTo: typeof LegendZoomTo;
60
+ readonly Opacity: {
61
+ Trigger: () => import("react/jsx-runtime").JSX.Element | null;
62
+ Popover: () => import("react/jsx-runtime").JSX.Element | null;
63
+ };
64
+ readonly ConfigSelect: typeof LegendConfigSelect;
65
+ readonly Item: typeof LegendItem;
66
+ readonly ItemUI: typeof LegendItemUI;
67
+ readonly Swatch: typeof LegendSwatch;
68
+ readonly CategoryUI: typeof LegendCategoryUI;
69
+ readonly RampUI: typeof LegendRampUI;
70
+ readonly ProportionUI: typeof LegendProportionUI;
71
+ readonly IconUI: typeof LegendIconUI;
72
+ };
73
+ export { DEFAULT_LEGEND_LABELS } from './provider';
74
+ export type { LegendProviderProps, LegendLabels } from './provider';
75
+ export type { LegendGroupProps } from './components/legend-group/legend-group';
76
+ export type { LegendRowProps } from './components/legend-row/legend-row';
77
+ export type { LegendActionsProps } from './components/legend-actions/legend-actions';
78
+ export type { LegendVisibilityToggleProps } from './components/legend-visibility-toggle/legend-visibility-toggle';
79
+ export type { LegendRowMenuProps } from './components/legend-row-menu/legend-row-menu';
80
+ export type { LegendZoomToProps } from './components/legend-row-menu/legend-zoom-to';
81
+ export type { LegendRowContextValue } from './components/contexts';
82
+ export type { LegendItemUIProps } from './components/legend-item/legend-item-ui';
83
+ export type { LegendSwatchProps } from './components/legend-swatch/legend-swatch';
84
+ export type { LegendCategoryUIProps } from './components/legend-category/legend-category-ui';
85
+ export type { LegendRampUIProps } from './components/legend-ramp/legend-ramp-ui';
86
+ export type { LegendProportionUIProps } from './components/legend-proportion/legend-proportion-ui';
87
+ export type { LegendIconUIProps } from './components/legend-icon/legend-icon-ui';
88
+ export type { LegendPosition, LegendVisibility, LegendOrientation, LegendSwatchShape, LegendFillPattern, LegendStrokeStyle, LegendVariable, LegendCategoryItem, LegendCategoryVariable, LegendRampStop, LegendRampVariable, LegendProportionVariable, LegendIconItem, LegendIconVariable, LegendConfigSection, LegendLayer, LegendGroup as LegendGroupModel, LegendLayerInput, LegendGroupInput, } from './stores';
@@ -0,0 +1,6 @@
1
+ export { LegendProvider } from './legend-provider';
2
+ export type { LegendProviderProps } from './legend-provider';
3
+ export { LegendConfigContext, useLegendConfig } from './legend-config-context';
4
+ export type { LegendConfig } from './legend-config-context';
5
+ export { DEFAULT_LEGEND_LABELS } from './labels';
6
+ export type { LegendLabels } from './labels';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Every user-facing string in the legend module. Override any subset via
3
+ * `<Legend.Provider labels={{ … }}>` — pure `*UI` components also accept a
4
+ * `labels` prop directly for store-free composition.
5
+ */
6
+ export interface LegendLabels {
7
+ expandLayer: string;
8
+ collapseLayer: string;
9
+ hideLayer: string;
10
+ showLayer: string;
11
+ layerOptions: string;
12
+ zoomTo: string;
13
+ hideGroup: string;
14
+ showGroup: string;
15
+ expandGroup: string;
16
+ collapseGroup: string;
17
+ opacity: string;
18
+ attribute: string;
19
+ radiusRangeBy: string;
20
+ colorBasedOn: string;
21
+ colorGradient: string;
22
+ /** Prefix for the largest proportion break, e.g. `"MAX: "`. */
23
+ maxPrefix: string;
24
+ /** Prefix for the smallest proportion break, e.g. `"MIN: "`. */
25
+ minPrefix: string;
26
+ }
27
+ export declare const DEFAULT_LEGEND_LABELS: LegendLabels;
@@ -0,0 +1,7 @@
1
+ import { LegendLabels } from './labels';
2
+ /** Per-panel configuration delivered by `Legend.Provider` to all containers. */
3
+ export interface LegendConfig {
4
+ labels: LegendLabels;
5
+ }
6
+ export declare const LegendConfigContext: import('react').Context<LegendConfig>;
7
+ export declare function useLegendConfig(): LegendConfig;
@@ -0,0 +1,38 @@
1
+ import { ReactNode } from 'react';
2
+ import { LegendGroupInput, LegendLayerInput } from '../stores';
3
+ import { LegendLabels } from './labels';
4
+ export interface LegendProviderProps {
5
+ /** Unique id for this legend panel — keys its store. */
6
+ id: string;
7
+ /**
8
+ * Layer definitions. Interactive fields (visible/opacity/…) are seeded.
9
+ * Pass stable references — `layers`, `groups`, and each layer's `variables`
10
+ * should be memoized or module constants (same contract as widgets-v2
11
+ * `data`); the store bails out of unchanged syncs by reference.
12
+ */
13
+ layers: LegendLayerInput[];
14
+ /** Optional group definitions for grouping layers. */
15
+ groups?: LegendGroupInput[];
16
+ /** Override any subset of the legend's user-facing strings. */
17
+ labels?: Partial<LegendLabels>;
18
+ /**
19
+ * When `true` (the default), the per-panel store survives unmount so
20
+ * visibility/opacity/order are preserved across remounts — the common case
21
+ * for a legend that toggles in and out of view. Consumers must call
22
+ * `deleteLegendStore(id)` when the legend is permanently removed. Set to
23
+ * `false` to tear the store down on unmount.
24
+ */
25
+ keepAlive?: boolean;
26
+ children: ReactNode;
27
+ }
28
+ /**
29
+ * Legend panel shell. Creates a per-panel Zustand store keyed by `id`, syncs
30
+ * the consumer's `layers`/`groups` definitions onto it (preserving interactive
31
+ * state), and exposes the `id` via `LegendContext` so descendants resolve their
32
+ * store with `useLegendId()`. Labels are surfaced via `LegendConfigContext`.
33
+ *
34
+ * Modeled on the widgets-v2 `Widget.Provider`.
35
+ *
36
+ * @experimental This API is new and may change in a future release.
37
+ */
38
+ export declare function LegendProvider({ id, layers, groups, labels, keepAlive, children, }: LegendProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ export type { LegendPosition, LegendOrientation, LegendSwatchShape, LegendFillPattern, LegendStrokeStyle, LegendCategoryItem, LegendCategoryVariable, LegendRampStop, LegendRampVariable, LegendProportionVariable, LegendIconItem, LegendIconVariable, LegendVariable, LegendConfigSection, LegendLayer, LegendGroup, LegendLayerInput, LegendGroupInput, LegendState, LegendStoreApi, LegendInit, } from './types';
2
+ export { LegendContext, useLegendId } from './legend-context';
3
+ export { createLegendStore, getLegendStore, hasLegendStore, deleteLegendStore, clearAllLegendStores, setLegendStoreEntry, registerLegendStore, unregisterLegendStore, useLegendStore, useLegendShallow, } from './legend-store-registry';
4
+ export type { LegendVisibility } from './selectors';
5
+ export { selectOrderedGroups, selectLayersByGroup, selectOrderedLayers, selectGroupVisibility, } from './selectors';
@@ -0,0 +1,4 @@
1
+ /** @experimental This API is new and may change in a future release. */
2
+ export declare const LegendContext: import('react').Context<string | null>;
3
+ /** @experimental This API is new and may change in a future release. */
4
+ export declare function useLegendId(): string;
@@ -0,0 +1,39 @@
1
+ import { LegendInit, LegendState, LegendStoreApi } from './types';
2
+ /**
3
+ * @internal — used by Provider; not part of the public API.
4
+ *
5
+ * @experimental This API is new and may change in a future release.
6
+ */
7
+ export declare function createLegendStore(id: string, init?: LegendInit): LegendStoreApi;
8
+ /** @experimental This API is new and may change in a future release. */
9
+ export declare function getLegendStore(id: string): LegendStoreApi;
10
+ /** @experimental This API is new and may change in a future release. */
11
+ export declare function hasLegendStore(id: string): boolean;
12
+ /** @experimental This API is new and may change in a future release. */
13
+ export declare function deleteLegendStore(id: string): void;
14
+ /** @experimental This API is new and may change in a future release. */
15
+ export declare function clearAllLegendStores(): void;
16
+ /**
17
+ * @internal — used by Provider's lazy init so children can resolve the store first render.
18
+ *
19
+ * @experimental This API is new and may change in a future release.
20
+ */
21
+ export declare function setLegendStoreEntry(id: string, store: LegendStoreApi): void;
22
+ /**
23
+ * @internal — registers a Provider mount and warns on duplicate ids in dev.
24
+ *
25
+ * @experimental This API is new and may change in a future release.
26
+ */
27
+ export declare function registerLegendStore(id: string, store: LegendStoreApi): void;
28
+ /**
29
+ * @internal — Provider unmount counterpart; removes the entry on last unmount.
30
+ *
31
+ * @experimental This API is new and may change in a future release.
32
+ */
33
+ export declare function unregisterLegendStore(id: string, options: {
34
+ keepAlive: boolean;
35
+ }): void;
36
+ /** @experimental This API is new and may change in a future release. */
37
+ export declare function useLegendStore<T>(id: string, selector: (state: LegendState) => T): T;
38
+ /** @experimental This API is new and may change in a future release. */
39
+ export declare function useLegendShallow<T>(id: string, selector: (state: LegendState) => T): T;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ import { LegendGroup, LegendLayer, LegendState } from './types';
2
+ /**
3
+ * Groups sorted by `order`.
4
+ *
5
+ * @experimental This API is new and may change in a future release.
6
+ */
7
+ export declare function selectOrderedGroups(state: LegendState): LegendGroup[];
8
+ /**
9
+ * Layers belonging to `groupId` (pass `undefined` for ungrouped, top-level
10
+ * layers), sorted by `order`.
11
+ *
12
+ * @experimental This API is new and may change in a future release.
13
+ */
14
+ export declare function selectLayersByGroup(state: LegendState, groupId: string | undefined): LegendLayer[];
15
+ /**
16
+ * All layers sorted by `order`, regardless of group.
17
+ *
18
+ * @experimental This API is new and may change in a future release.
19
+ */
20
+ export declare function selectOrderedLayers(state: LegendState): LegendLayer[];
21
+ /**
22
+ * Tri-state visibility — derived for groups from their member layers, and the
23
+ * state contract of a controlled `Legend.VisibilityToggle` for any custom
24
+ * element kind.
25
+ */
26
+ export type LegendVisibility = 'visible' | 'hidden' | 'mixed';
27
+ /**
28
+ * Tri-state group visibility: `'visible'` when every member layer is visible
29
+ * (also for empty groups), `'hidden'` when none is, `'mixed'` otherwise.
30
+ *
31
+ * @experimental This API is new and may change in a future release.
32
+ */
33
+ export declare function selectGroupVisibility(state: LegendState, groupId: string): LegendVisibility;
@@ -0,0 +1,275 @@
1
+ import { ReactNode } from 'react';
2
+ import { StoreApi } from 'zustand';
3
+ /** Floating panel anchor position. */
4
+ export type LegendPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
5
+ /**
6
+ * Layout direction of a category/icon legend's items. `'horizontal'` flows
7
+ * the swatch+label pairs left-to-right, wrapping onto multiple rows.
8
+ */
9
+ export type LegendOrientation = 'vertical' | 'horizontal';
10
+ /** Swatch shape for a category legend. */
11
+ export type LegendSwatchShape = 'square' | 'circle' | 'line';
12
+ /** Fill pattern for a filled square/circle swatch (`isStrokeColor: false`). */
13
+ export type LegendFillPattern = 'solid' | 'horizontal' | 'vertical' | 'diagonal' | 'diagonal-alt' | 'cross' | 'dots' | 'grid';
14
+ /**
15
+ * Border style for a stroked square/circle swatch (`isStrokeColor: true`), or
16
+ * the line style for `shape: 'line'`. `corners` applies to squares only;
17
+ * `dash-dot` applies to lines only — inapplicable values degrade gracefully.
18
+ */
19
+ export type LegendStrokeStyle = 'solid' | 'dashed' | 'dotted' | 'dash-dot' | 'corners';
20
+ export interface LegendCategoryItem {
21
+ label: string;
22
+ /** Fill or stroke color (see `isStrokeColor`). */
23
+ color: string;
24
+ /**
25
+ * Optional value shown alongside the label — a pre-formatted node, so the
26
+ * consumer controls the formatting (e.g. `'12%'`, `'1.2K'`, or any element).
27
+ */
28
+ value?: ReactNode;
29
+ /** Optional marker diameter/length in px (the "Small/Medium/Large/XL" sizes). */
30
+ size?: number;
31
+ /** Override the variable-level swatch shape for this item. */
32
+ shape?: LegendSwatchShape;
33
+ /** Override the variable-level fill pattern for this item (mix patterns in one list). */
34
+ fillPattern?: LegendFillPattern;
35
+ /** Override the variable-level border/line style for this item. */
36
+ strokeStyle?: LegendStrokeStyle;
37
+ /** Override the variable-level fill-vs-outline mode for this item. */
38
+ isStrokeColor?: boolean;
39
+ }
40
+ export interface LegendCategoryVariable {
41
+ type: 'category';
42
+ items: LegendCategoryItem[];
43
+ /** Default swatch shape for all items (each item may override). Default `'circle'`. */
44
+ shape?: LegendSwatchShape;
45
+ /**
46
+ * Default fill-vs-outline mode (each item may override). When true, `color`
47
+ * paints the marker border/outline instead of its fill.
48
+ */
49
+ isStrokeColor?: boolean;
50
+ /**
51
+ * Default fill pattern when filled (each item may override — mix patterns
52
+ * in one list). Default `'solid'`.
53
+ */
54
+ fillPattern?: LegendFillPattern;
55
+ /**
56
+ * Default border style when stroked, or line style for `shape: 'line'`
57
+ * (each item may override). Default `'solid'`.
58
+ */
59
+ strokeStyle?: LegendStrokeStyle;
60
+ /** Item layout direction. Default `'vertical'`. */
61
+ orientation?: LegendOrientation;
62
+ }
63
+ export interface LegendRampStop {
64
+ label: string;
65
+ value: number;
66
+ }
67
+ export interface LegendRampVariable {
68
+ type: 'ramp';
69
+ /** Ordered low→high colors (hex / rgb / css). */
70
+ colors: string[];
71
+ /**
72
+ * Ordered low→high stops. Discrete ramps **require `colors.length + 1`**
73
+ * stops — the boundaries between color steps, including both outer edges —
74
+ * and each label is centered on its boundary line (keep labels short, e.g.
75
+ * `20K`; there is no collision handling). A discrete ramp with any other
76
+ * stop count is incoherent and renders an inline error instead of the bar.
77
+ * Continuous ramps show only the first/last endpoints.
78
+ */
79
+ stops: LegendRampStop[];
80
+ /** Continuous gradient bar vs. discrete color steps. */
81
+ isContinuous: boolean;
82
+ /**
83
+ * Average value, marked as a dashed vertical line on the bar (tooltip shows
84
+ * the formatted value). Positioned linearly between the first and last stop
85
+ * `value`s; out-of-range averages clamp to the bar's edge.
86
+ */
87
+ avg?: number;
88
+ /**
89
+ * Formats `avg` for its tooltip. Default `String(avg)` — pass e.g.
90
+ * `(v) => formatNumber(v, 'es-ES')` or `(v) => `${v}%``. Use a stable
91
+ * reference (same contract as the other variable fields).
92
+ */
93
+ formatValue?: (value: number) => ReactNode;
94
+ /** Attribute the color is based on (shown as "Color based on"). */
95
+ attribute?: string;
96
+ }
97
+ export interface LegendProportionVariable {
98
+ type: 'proportion';
99
+ min: number;
100
+ max: number;
101
+ /**
102
+ * Explicit value breaks to label (order-insensitive — the renderer sorts
103
+ * descending and caps at the **4 largest**). When omitted (or fewer than 2
104
+ * entries), breaks are evenly spaced between `min` and `max` over `steps`.
105
+ */
106
+ stops?: number[];
107
+ /**
108
+ * Number of nested circles and derived value breaks, clamped to **2–4**.
109
+ * Defaults to 4. Ignored when explicit `stops` are provided (their count,
110
+ * capped at 4, wins). Circles and labels always match 1:1. Edge case: when
111
+ * `min === max` the renderer collapses to a **single** circle/break (the
112
+ * shared value), regardless of `steps`.
113
+ */
114
+ steps?: number;
115
+ /**
116
+ * Formats each derived break (MAX / middle / MIN). Default `String(value)`
117
+ * — pass e.g. `(v) => formatNumber(v, 'es-ES')`. Use a stable reference
118
+ * (same contract as the other variable fields).
119
+ */
120
+ formatValue?: (value: number) => ReactNode;
121
+ /** Attribute the size is based on (shown as "Radius range by"). */
122
+ attribute?: string;
123
+ }
124
+ export interface LegendIconItem {
125
+ label: string;
126
+ /** Icon URL (data URI or external). */
127
+ icon: string;
128
+ /**
129
+ * Optional value shown alongside the label — a pre-formatted node, so the
130
+ * consumer controls the formatting (e.g. `'12%'`, `'1.2K'`, or any element).
131
+ */
132
+ value?: ReactNode;
133
+ }
134
+ export interface LegendIconVariable {
135
+ type: 'icon';
136
+ items: LegendIconItem[];
137
+ /** Item layout direction. Default `'vertical'`. */
138
+ orientation?: LegendOrientation;
139
+ }
140
+ export type LegendVariable = LegendCategoryVariable | LegendRampVariable | LegendProportionVariable | LegendIconVariable;
141
+ /**
142
+ * One section of a layer's configuration select. The select's menu lists each
143
+ * section under its `title` (omitted when the layer has a single section);
144
+ * every section keeps its own highlighted `active` option. The closed field
145
+ * displays the first section's `active`.
146
+ */
147
+ export interface LegendConfigSection {
148
+ id: string;
149
+ /** Section heading in the menu (e.g. "Global"). Hidden for a single section. */
150
+ title?: string;
151
+ options: string[];
152
+ /** The section's currently-selected option. */
153
+ active: string;
154
+ }
155
+ /** A layer row in the legend. Holds both static definition and interactive state. */
156
+ export interface LegendLayer {
157
+ id: string;
158
+ name: string;
159
+ /** Secondary line (e.g. "Zoom level 1 – 12"). */
160
+ subtitle?: string;
161
+ visible: boolean;
162
+ /** 0–1. */
163
+ opacity: number;
164
+ collapsed: boolean;
165
+ /**
166
+ * @internal Collapsed state snapshotted when the layer was hidden — restored
167
+ * on show so a manual collapse/expand persists across visibility toggles.
168
+ * Managed by `setVisibility`/`setCollapsed`; do not set from outside.
169
+ */
170
+ collapsedBeforeHide?: boolean;
171
+ /** Sort order among siblings (lower = first). */
172
+ order: number;
173
+ /** Owning group id, if the layer belongs to a group. */
174
+ groupId?: string;
175
+ /** Show the opacity header action (slider + input popover) for this layer. */
176
+ opacityControl?: boolean;
177
+ /**
178
+ * Configuration-select sections. When absent (or empty), the row renders no
179
+ * select. User-picked `active`s survive Provider re-syncs as long as the
180
+ * section id still exists and the pick is still among its `options`.
181
+ */
182
+ sections?: LegendConfigSection[];
183
+ /** Footer helper note — one per layer, shown after all variables. */
184
+ helperText?: ReactNode;
185
+ /**
186
+ * Ordered item renderers, stacked top-to-bottom in the row body (a layer
187
+ * can mix e.g. two category lists and a proportion). Pass a stable array
188
+ * reference — `_sync` compares it by reference, like the other prop-driven
189
+ * fields.
190
+ */
191
+ variables: LegendVariable[];
192
+ }
193
+ /**
194
+ * A group of layer rows. A group has no stored visibility of its own — it is
195
+ * derived from its member layers (see `selectGroupVisibility`): all visible →
196
+ * `'visible'`, none visible → `'hidden'`, otherwise `'mixed'`.
197
+ */
198
+ export interface LegendGroup {
199
+ id: string;
200
+ label: string;
201
+ collapsed: boolean;
202
+ /**
203
+ * @internal Collapsed state snapshotted when the group was hidden via
204
+ * `setGroupVisibility` — restored on show. Managed by the setters.
205
+ */
206
+ collapsedBeforeHide?: boolean;
207
+ order: number;
208
+ }
209
+ /** @experimental This API is new and may change in a future release. */
210
+ export interface LegendLayerInput extends Omit<LegendLayer, 'visible' | 'opacity' | 'collapsed' | 'collapsedBeforeHide' | 'order'> {
211
+ visible?: boolean;
212
+ opacity?: number;
213
+ collapsed?: boolean;
214
+ order?: number;
215
+ }
216
+ /** @experimental This API is new and may change in a future release. */
217
+ export interface LegendGroupInput extends Omit<LegendGroup, 'collapsed' | 'collapsedBeforeHide' | 'order'> {
218
+ collapsed?: boolean;
219
+ order?: number;
220
+ }
221
+ export interface LegendState {
222
+ layers: Record<string, LegendLayer>;
223
+ groups: Record<string, LegendGroup>;
224
+ setVisibility: (id: string, visible: boolean) => void;
225
+ setOpacity: (id: string, opacity: number) => void;
226
+ setCollapsed: (id: string, collapsed: boolean) => void;
227
+ /**
228
+ * Pick a section's active option in the layer's configuration select.
229
+ * No-op for unknown layers/sections or when `value` is not one of the
230
+ * section's `options`.
231
+ */
232
+ setSectionValue: (id: string, sectionId: string, value: string) => void;
233
+ /**
234
+ * Show/hide **all member layers** of a group (the group itself has no stored
235
+ * visibility — it is derived from its members). Each member keeps its own
236
+ * collapse snapshot; the group additionally collapses when hiding and
237
+ * restores its pre-hide collapse state when showing. No-op when the group
238
+ * is already at the target state or has no members.
239
+ */
240
+ setGroupVisibility: (id: string, visible: boolean) => void;
241
+ setGroupCollapsed: (id: string, collapsed: boolean) => void;
242
+ /**
243
+ * Add a layer, or update it if `input.id` already exists. Interactive state
244
+ * (visible/opacity/collapsed/order and section actives) is preserved unless
245
+ * the input explicitly overrides it; new layers append to the end of their
246
+ * sibling bucket. No-op when an existing layer is unchanged.
247
+ */
248
+ setLegendLayer: (input: LegendLayerInput) => void;
249
+ /**
250
+ * Remove a layer and densely re-number the remaining siblings. No-op for an
251
+ * unknown id.
252
+ */
253
+ removeLegendLayer: (id: string) => void;
254
+ /**
255
+ * Add a group, or update it if `input.id` already exists (collapse/order
256
+ * preserved on update). No-op when unchanged.
257
+ */
258
+ setLegendGroup: (input: LegendGroupInput) => void;
259
+ /**
260
+ * Remove a group; its member layers are **un-grouped** (their `groupId` is
261
+ * cleared) so they survive as ungrouped rows rather than vanish. No-op for an
262
+ * unknown id.
263
+ */
264
+ removeLegendGroup: (id: string) => void;
265
+ /**
266
+ * @internal Provider prop-sync. Adds new entities, updates static fields of
267
+ * known ones (preserving their interactive state), and removes vanished ones.
268
+ */
269
+ _sync: (layers: LegendLayerInput[], groups: LegendGroupInput[]) => void;
270
+ }
271
+ export type LegendStoreApi = StoreApi<LegendState>;
272
+ export interface LegendInit {
273
+ layers?: LegendLayerInput[];
274
+ groups?: LegendGroupInput[];
275
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "4.12.2",
3
+ "version": "4.13.0",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -31,8 +31,8 @@
31
31
  "@mui/x-date-pickers": "^7.0.0",
32
32
  "echarts": "^6.0.0",
33
33
  "html2canvas": "^1.4.1",
34
- "react": "^18.0.0 || ^19.0.0",
35
- "react-dom": "^18.0.0 || ^19.0.0",
34
+ "react": "^19.2.0",
35
+ "react-dom": "^19.2.0",
36
36
  "react-markdown": "^10.0.0",
37
37
  "zustand": "^5.0.0",
38
38
  "@carto/ps-utils": "2.0.1"
@@ -46,6 +46,14 @@
46
46
  "import": "./dist/components.js",
47
47
  "types": "./dist/types/components/index.d.ts"
48
48
  },
49
+ "./legend": {
50
+ "import": "./dist/legend.js",
51
+ "types": "./dist/types/legend/index.d.ts"
52
+ },
53
+ "./legend/stores": {
54
+ "import": "./dist/legend/stores.js",
55
+ "types": "./dist/types/legend/stores/index.d.ts"
56
+ },
49
57
  "./widgets/actions": {
50
58
  "import": "./dist/widgets/actions.js",
51
59
  "types": "./dist/types/widgets/actions/index.d.ts"
@@ -111,6 +111,21 @@ describe('GeolocationControls', () => {
111
111
  expect(mockOnChange).toHaveBeenCalledWith(mockPosition.coords)
112
112
  })
113
113
 
114
+ test('does not re-query permissions when only the onError identity changes', () => {
115
+ mockPermissions.query.mockResolvedValue(mockPermissions)
116
+
117
+ const { rerender } = render(
118
+ <GeolocationControls onChange={mockOnChange} onError={vi.fn()} />,
119
+ )
120
+ expect(mockPermissions.query).toHaveBeenCalledTimes(1)
121
+
122
+ // `onError` is read through an Effect Event, so the permission-query
123
+ // effect depends on `hasGeolocation` alone. A fresh callback identity
124
+ // must NOT re-run the query or re-register the permission listener.
125
+ rerender(<GeolocationControls onChange={mockOnChange} onError={vi.fn()} />)
126
+ expect(mockPermissions.query).toHaveBeenCalledTimes(1)
127
+ })
128
+
114
129
  test('handles geolocation errors', () => {
115
130
  mockPermissions.query.mockResolvedValue(mockPermissions)
116
131