@carto/ps-react-ui 4.12.1 → 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 (104) 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/dist/widgets-v2/bar.js +7 -3
  51. package/dist/widgets-v2/bar.js.map +1 -1
  52. package/dist/widgets-v2/histogram.js +9 -4
  53. package/dist/widgets-v2/histogram.js.map +1 -1
  54. package/package.json +11 -3
  55. package/src/components/geolocation-controls/geolocation-controls.test.tsx +15 -0
  56. package/src/components/geolocation-controls/geolocation-controls.tsx +19 -3
  57. package/src/components/index.ts +2 -0
  58. package/src/components/responsive-drawer/responsive-drawer.test.tsx +129 -25
  59. package/src/components/responsive-drawer/responsive-drawer.tsx +149 -16
  60. package/src/legend/components/contexts.ts +32 -0
  61. package/src/legend/components/icons.tsx +22 -0
  62. package/src/legend/components/index.ts +39 -0
  63. package/src/legend/components/legend-actions/legend-actions.tsx +57 -0
  64. package/src/legend/components/legend-actions/styles.ts +9 -0
  65. package/src/legend/components/legend-category/legend-category-ui.tsx +91 -0
  66. package/src/legend/components/legend-category/styles.ts +61 -0
  67. package/src/legend/components/legend-config-select/legend-config-select.tsx +96 -0
  68. package/src/legend/components/legend-config-select/styles.ts +48 -0
  69. package/src/legend/components/legend-group/legend-group.test.tsx +115 -0
  70. package/src/legend/components/legend-group/legend-group.tsx +88 -0
  71. package/src/legend/components/legend-group/styles.ts +82 -0
  72. package/src/legend/components/legend-icon/legend-icon-ui.tsx +56 -0
  73. package/src/legend/components/legend-icon/styles.ts +56 -0
  74. package/src/legend/components/legend-item/legend-item-ui.test.tsx +362 -0
  75. package/src/legend/components/legend-item/legend-item-ui.tsx +43 -0
  76. package/src/legend/components/legend-item/legend-item.tsx +34 -0
  77. package/src/legend/components/legend-item/styles.ts +11 -0
  78. package/src/legend/components/legend-opacity/legend-opacity.tsx +179 -0
  79. package/src/legend/components/legend-opacity/styles.ts +28 -0
  80. package/src/legend/components/legend-proportion/legend-proportion-ui.tsx +122 -0
  81. package/src/legend/components/legend-proportion/styles.ts +43 -0
  82. package/src/legend/components/legend-ramp/legend-ramp-ui.tsx +159 -0
  83. package/src/legend/components/legend-ramp/styles.ts +62 -0
  84. package/src/legend/components/legend-row/legend-row.test.tsx +498 -0
  85. package/src/legend/components/legend-row/legend-row.tsx +129 -0
  86. package/src/legend/components/legend-row/styles.ts +111 -0
  87. package/src/legend/components/legend-row-menu/legend-row-menu.tsx +68 -0
  88. package/src/legend/components/legend-row-menu/legend-zoom-to.tsx +35 -0
  89. package/src/legend/components/legend-swatch/legend-swatch.test.tsx +109 -0
  90. package/src/legend/components/legend-swatch/legend-swatch.tsx +237 -0
  91. package/src/legend/components/legend-visibility-toggle/legend-visibility-toggle.tsx +127 -0
  92. package/src/legend/index.ts +121 -0
  93. package/src/legend/provider/index.ts +6 -0
  94. package/src/legend/provider/labels.ts +59 -0
  95. package/src/legend/provider/legend-config-context.ts +15 -0
  96. package/src/legend/provider/legend-provider.tsx +102 -0
  97. package/src/legend/stores/index.ts +46 -0
  98. package/src/legend/stores/legend-context.ts +17 -0
  99. package/src/legend/stores/legend-store-registry.ts +639 -0
  100. package/src/legend/stores/legend-store.test.ts +457 -0
  101. package/src/legend/stores/selectors.ts +65 -0
  102. package/src/legend/stores/types.ts +347 -0
  103. package/src/widgets-v2/bar/options.ts +4 -0
  104. package/src/widgets-v2/histogram/options.ts +5 -0
@@ -0,0 +1,28 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ // Tooltip wrapper around the (possibly disabled) trigger button. A plain
5
+ // inline span would be sized by the line box (baseline + descender) and
6
+ // end up ~1px taller than the 24px button inside.
7
+ triggerWrap: {
8
+ display: 'inline-flex',
9
+ },
10
+ popover: {
11
+ display: 'flex',
12
+ alignItems: 'center',
13
+ gap: 1.5,
14
+ padding: ({ spacing }) => spacing(1, 1.5),
15
+ width: 280,
16
+ },
17
+ slider: {
18
+ flex: 1,
19
+ },
20
+ // Wide enough for "100 %" — the value must never ellipsize.
21
+ input: {
22
+ width: 76,
23
+ flexShrink: 0,
24
+ '& input': {
25
+ textAlign: 'right',
26
+ },
27
+ },
28
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,122 @@
1
+ import type { ReactNode } from 'react'
2
+ import { Box, Typography } from '@mui/material'
3
+ import { styles } from './styles'
4
+ import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels'
5
+ import type { LegendProportionVariable } from '../../stores'
6
+
7
+ export interface LegendProportionUIProps {
8
+ data: LegendProportionVariable
9
+ /** Override the renderer's user-facing strings. */
10
+ labels?: Pick<LegendLabels, 'radiusRangeBy' | 'maxPrefix' | 'minPrefix'>
11
+ }
12
+
13
+ const MAX_DIAMETER = 72
14
+ const RATIO_FLOOR = 0.3
15
+ const MIN_STEPS = 2
16
+ const MAX_STEPS = 4
17
+
18
+ /**
19
+ * Value breaks, largest→smallest. Explicit `stops` (≥2 entries) win, capped at
20
+ * the 4 largest; otherwise breaks are derived evenly from `min`/`max` over
21
+ * `steps` clamped to 2–4. Circles map 1:1 to the breaks.
22
+ */
23
+ function resolveBreaks(data: LegendProportionVariable): number[] {
24
+ const explicit = (data.stops ?? []).filter((n) => Number.isFinite(n))
25
+ if (explicit.length >= MIN_STEPS) {
26
+ return explicit.sort((a, b) => b - a).slice(0, MAX_STEPS)
27
+ }
28
+ const { min, max } = data
29
+ if (min === max) return [max]
30
+ const steps = Math.min(
31
+ MAX_STEPS,
32
+ Math.max(MIN_STEPS, data.steps ?? MAX_STEPS),
33
+ )
34
+ return Array.from(
35
+ { length: steps },
36
+ (_, i) => max - ((max - min) * i) / (steps - 1),
37
+ )
38
+ }
39
+
40
+ /** Decorative nested-circle diameter for circle `i` of `n` (i=0 outermost). */
41
+ function diameterFor(i: number, n: number): number {
42
+ if (n <= 1) return MAX_DIAMETER
43
+ const ratio = 1 - (1 - RATIO_FLOOR) * (i / (n - 1))
44
+ return MAX_DIAMETER * ratio
45
+ }
46
+
47
+ /**
48
+ * Proportional-symbol legend — a stack of decorative nested circles with the
49
+ * value breaks labelled beside them (largest = MAX, smallest = MIN). Circle
50
+ * sizes are fixed/decorative, not value-derived, so the legend stays readable
51
+ * across very wide ranges.
52
+ *
53
+ * @experimental This API is new and may change in a future release.
54
+ */
55
+ export function LegendProportionUI({
56
+ data,
57
+ labels = DEFAULT_LEGEND_LABELS,
58
+ }: LegendProportionUIProps) {
59
+ const { attribute, formatValue = String } = data
60
+ const breaks = resolveBreaks(data)
61
+ const n = breaks.length
62
+
63
+ const labelFor = (value: number, i: number): ReactNode => {
64
+ const formatted = formatValue(value)
65
+ if (n <= 1) return formatted
66
+ if (i === 0)
67
+ return (
68
+ <>
69
+ {labels.maxPrefix}
70
+ {formatted}
71
+ </>
72
+ )
73
+ if (i === n - 1)
74
+ return (
75
+ <>
76
+ {labels.minPrefix}
77
+ {formatted}
78
+ </>
79
+ )
80
+ return formatted
81
+ }
82
+
83
+ return (
84
+ <Box sx={styles.root}>
85
+ {attribute && (
86
+ <Box sx={styles.attribute}>
87
+ <Typography variant='overline' color='text.secondary'>
88
+ {labels.radiusRangeBy}
89
+ </Typography>
90
+ <Typography variant='caption' color='text.primary'>
91
+ {attribute}
92
+ </Typography>
93
+ </Box>
94
+ )}
95
+
96
+ <Box sx={styles.body}>
97
+ <Box
98
+ sx={{ ...styles.circles, width: MAX_DIAMETER, height: MAX_DIAMETER }}
99
+ >
100
+ {breaks.map((_, i) => {
101
+ const diameter = diameterFor(i, n)
102
+ return (
103
+ <Box
104
+ key={i}
105
+ aria-hidden
106
+ sx={{ ...styles.circle, width: diameter, height: diameter }}
107
+ />
108
+ )
109
+ })}
110
+ </Box>
111
+
112
+ <Box sx={{ ...styles.labels, height: MAX_DIAMETER }}>
113
+ {breaks.map((value, i) => (
114
+ <Typography key={i} variant='caption' color='text.secondary'>
115
+ {labelFor(value, i)}
116
+ </Typography>
117
+ ))}
118
+ </Box>
119
+ </Box>
120
+ </Box>
121
+ )
122
+ }
@@ -0,0 +1,43 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ root: {
5
+ display: 'flex',
6
+ flexDirection: 'column',
7
+ gap: 1,
8
+ },
9
+ attribute: {
10
+ display: 'flex',
11
+ flexDirection: 'column',
12
+ },
13
+ body: {
14
+ display: 'flex',
15
+ alignItems: 'flex-end',
16
+ gap: 2,
17
+ paddingTop: 0.5,
18
+ },
19
+ circles: {
20
+ position: 'relative',
21
+ flexShrink: 0,
22
+ },
23
+ // All circles share the bottom and right edges (anchored bottom-right), so
24
+ // they nest along the diagonal — largest fanning up-left, smallest in the
25
+ // bottom-right. Every circle has the SAME opaque fill, so overlaps don't
26
+ // compound (uniform colour) while the borders still read as nested rings.
27
+ circle: {
28
+ position: 'absolute',
29
+ right: 0,
30
+ bottom: 0,
31
+ borderRadius: '50%',
32
+ backgroundColor: 'grey.50',
33
+ border: '1px solid',
34
+ borderColor: 'grey.100',
35
+ boxSizing: 'border-box',
36
+ },
37
+ labels: {
38
+ flex: 1,
39
+ display: 'flex',
40
+ flexDirection: 'column',
41
+ justifyContent: 'space-between',
42
+ },
43
+ } satisfies Record<string, SxProps<Theme>>
@@ -0,0 +1,159 @@
1
+ import { Box, Tooltip, Typography } from '@mui/material'
2
+ import { ErrorOutline as ErrorIcon } from '@mui/icons-material'
3
+ import { styles } from './styles'
4
+ import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels'
5
+ import type { LegendRampVariable } from '../../stores'
6
+
7
+ export interface LegendRampUIProps {
8
+ data: LegendRampVariable
9
+ /** Override the renderer's user-facing strings. */
10
+ labels?: Pick<LegendLabels, 'colorBasedOn' | 'colorGradient'>
11
+ }
12
+
13
+ /**
14
+ * Horizontal position (0–100) of `avg` on the bar — linear between the first
15
+ * and last stop values, clamped to the edges. `null` when there's no scale
16
+ * (fewer than 2 stops or a zero-width range).
17
+ */
18
+ function avgPercent(data: LegendRampVariable): number | null {
19
+ const { avg, stops } = data
20
+ if (avg == null || stops.length < 2) return null
21
+ const v0 = stops[0]!.value
22
+ const vn = stops[stops.length - 1]!.value
23
+ if (v0 === vn) return null
24
+ const pct = ((avg - v0) / (vn - v0)) * 100
25
+ return Math.min(100, Math.max(0, pct))
26
+ }
27
+
28
+ /**
29
+ * Color-ramp legend. `colors` and `stops` are both ordered low→high and are
30
+ * never reversed, so step colors and their labels stay aligned.
31
+ *
32
+ * Continuous mode renders a gradient with the endpoint labels. Discrete mode
33
+ * **requires `stops.length === colors.length + 1`** — stops are the boundaries
34
+ * between color steps (including both outer edges) and each label is centered
35
+ * on its boundary line. A discrete ramp with any other stop count is an
36
+ * incoherent state and renders a compact inline error instead of the bar.
37
+ *
38
+ * @experimental This API is new and may change in a future release.
39
+ */
40
+ export function LegendRampUI({
41
+ data,
42
+ labels = DEFAULT_LEGEND_LABELS,
43
+ }: LegendRampUIProps) {
44
+ const { colors, stops, isContinuous, attribute, formatValue = String } = data
45
+ const avgPct = avgPercent(data)
46
+
47
+ const first = stops[0]
48
+ const last = stops[stops.length - 1]
49
+ const gradient = `linear-gradient(90deg, ${colors.join(', ')})`
50
+
51
+ // Discrete ramps need one stop per color boundary (colors.length + 1).
52
+ // `isBoundary` is therefore equivalent to "valid discrete".
53
+ const isBoundary = !isContinuous && stops.length === colors.length + 1
54
+ const invalidDiscrete = !isContinuous && !isBoundary
55
+
56
+ // Boundary range for assistive tech (valid discrete only reaches here).
57
+ const stepAriaLabel = (index: number): string =>
58
+ `${stops[index]?.label ?? ''} – ${stops[index + 1]?.label ?? ''}`
59
+
60
+ const attributeHeader = attribute && (
61
+ <Box sx={styles.attribute}>
62
+ <Typography variant='overline' color='text.secondary'>
63
+ {labels.colorBasedOn}
64
+ </Typography>
65
+ <Typography variant='caption' color='text.primary'>
66
+ {attribute}
67
+ </Typography>
68
+ </Box>
69
+ )
70
+
71
+ if (invalidDiscrete) {
72
+ return (
73
+ <Box sx={styles.container}>
74
+ {attributeHeader}
75
+ <Box sx={styles.error}>
76
+ <ErrorIcon fontSize='small' />
77
+ <Typography variant='caption' color='error'>
78
+ {`Discrete ramp needs ${colors.length + 1} stops (one per color boundary) but got ${stops.length}.`}
79
+ </Typography>
80
+ </Box>
81
+ </Box>
82
+ )
83
+ }
84
+
85
+ return (
86
+ <Box sx={styles.container}>
87
+ {attributeHeader}
88
+
89
+ <Box sx={styles.barWrap}>
90
+ <Box sx={styles.bar}>
91
+ {isContinuous ? (
92
+ <Box
93
+ role='img'
94
+ aria-label={labels.colorGradient}
95
+ sx={{ width: '100%', height: '100%', background: gradient }}
96
+ />
97
+ ) : (
98
+ colors.map((color, index) => (
99
+ <Box
100
+ key={index}
101
+ role='img'
102
+ aria-label={stepAriaLabel(index)}
103
+ sx={{ ...styles.step, backgroundColor: color }}
104
+ />
105
+ ))
106
+ )}
107
+ </Box>
108
+
109
+ {avgPct != null && data.avg != null && (
110
+ <Tooltip title={formatValue(data.avg)} placement='top' arrow>
111
+ {/* Inline style: per-instance dynamic value, no emotion class churn. */}
112
+ <Box sx={styles.avgLine} style={{ left: `${avgPct}%` }} />
113
+ </Tooltip>
114
+ )}
115
+ </Box>
116
+
117
+ {isBoundary ? (
118
+ <Box sx={styles.boundaryLabels}>
119
+ {stops.map((stop, index) => {
120
+ // Interior labels center on their boundary line; the outer two
121
+ // clamp inside the bar (first left-aligned, last right-aligned)
122
+ // so the row never overflows.
123
+ const isLast = index === stops.length - 1
124
+ const transform =
125
+ index === 0
126
+ ? undefined
127
+ : isLast
128
+ ? 'translateX(-100%)'
129
+ : 'translateX(-50%)'
130
+ return (
131
+ <Typography
132
+ key={`${stop.label}-${index}`}
133
+ variant='overline'
134
+ color='text.secondary'
135
+ sx={styles.boundaryLabel}
136
+ style={{
137
+ left: `${(index / colors.length) * 100}%`,
138
+ transform,
139
+ }}
140
+ >
141
+ {stop.label}
142
+ </Typography>
143
+ )
144
+ })}
145
+ </Box>
146
+ ) : (
147
+ // Continuous gradients show only the range endpoints.
148
+ <Box sx={styles.labels}>
149
+ <Typography variant='overline' color='text.secondary'>
150
+ {first?.label}
151
+ </Typography>
152
+ <Typography variant='overline' color='text.secondary'>
153
+ {last?.label}
154
+ </Typography>
155
+ </Box>
156
+ )}
157
+ </Box>
158
+ )
159
+ }
@@ -0,0 +1,62 @@
1
+ import type { SxProps, Theme } from '@mui/material'
2
+
3
+ export const styles = {
4
+ container: {
5
+ display: 'flex',
6
+ flexDirection: 'column',
7
+ gap: 0.5,
8
+ },
9
+ attribute: {
10
+ display: 'flex',
11
+ flexDirection: 'column',
12
+ },
13
+ // Inline error shown when a discrete ramp's stop count is incoherent.
14
+ error: {
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ gap: 0.5,
18
+ color: 'error.main',
19
+ },
20
+ // Positioning context for the avg marker; the inner bar keeps its rounded
21
+ // clip while the line is free to overflow vertically.
22
+ barWrap: {
23
+ position: 'relative',
24
+ },
25
+ bar: {
26
+ height: 8,
27
+ borderRadius: '2px',
28
+ overflow: 'hidden',
29
+ display: 'flex',
30
+ },
31
+ avgLine: {
32
+ position: 'absolute',
33
+ top: -2,
34
+ bottom: -2,
35
+ width: 0,
36
+ borderLeft: '1.5px dashed',
37
+ borderColor: 'text.primary',
38
+ transform: 'translateX(-50%)',
39
+ },
40
+ step: {
41
+ flex: 1,
42
+ height: '100%',
43
+ },
44
+ labels: {
45
+ display: 'flex',
46
+ justifyContent: 'space-between',
47
+ gap: 1,
48
+ },
49
+ // Boundary mode: labels absolutely positioned so each centers on the line
50
+ // between adjacent color steps. The outer two clamp inside the bar's width
51
+ // (first left-aligned, last right-aligned) so nothing overflows the row.
52
+ // `left`/`transform` are applied inline per label in the renderer.
53
+ boundaryLabels: {
54
+ position: 'relative',
55
+ height: 16,
56
+ },
57
+ boundaryLabel: {
58
+ position: 'absolute',
59
+ top: 0,
60
+ whiteSpace: 'nowrap',
61
+ },
62
+ } satisfies Record<string, SxProps<Theme>>