@carto/ps-react-ui 6.3.0 → 6.5.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 (113) hide show
  1. package/dist/actions-D_PVFZaM.js +1272 -0
  2. package/dist/actions-D_PVFZaM.js.map +1 -0
  3. package/dist/{category-DULnoxvk.js → category-UIGvFN3D.js} +263 -246
  4. package/dist/category-UIGvFN3D.js.map +1 -0
  5. package/dist/chat.js +472 -470
  6. package/dist/chat.js.map +1 -1
  7. package/dist/{components-CTUpdQuj.js → components-DDJU4m0t.js} +8 -8
  8. package/dist/components-DDJU4m0t.js.map +1 -0
  9. package/dist/components.js +2 -2
  10. package/dist/{download-B4sykOFm.js → download-DIj1Iz0t.js} +2 -2
  11. package/dist/{download-B4sykOFm.js.map → download-DIj1Iz0t.js.map} +1 -1
  12. package/dist/{formula-DJS-JLpk.js → formula-CSHhAoyW.js} +4 -4
  13. package/dist/{formula-DJS-JLpk.js.map → formula-CSHhAoyW.js.map} +1 -1
  14. package/dist/{item-color-B7xkIpDZ.js → item-color-7o7pFBR2.js} +26 -18
  15. package/dist/item-color-7o7pFBR2.js.map +1 -0
  16. package/dist/markdown-B8lLR-NY.js +106 -0
  17. package/dist/markdown-B8lLR-NY.js.map +1 -0
  18. package/dist/scale-Bhx8HKEB.js +29 -0
  19. package/dist/scale-Bhx8HKEB.js.map +1 -0
  20. package/dist/{spread-CQwFIwuz.js → spread-BRfPPIep.js} +3 -3
  21. package/dist/{spread-CQwFIwuz.js.map → spread-BRfPPIep.js.map} +1 -1
  22. package/dist/style-DTLuIo84.js +87 -0
  23. package/dist/style-DTLuIo84.js.map +1 -0
  24. package/dist/{styles-BCEBm7xh.js → styles-yKcpfyNV.js} +5 -5
  25. package/dist/styles-yKcpfyNV.js.map +1 -0
  26. package/dist/{table-DEdW_fDj.js → table-Col_FRvb.js} +2 -2
  27. package/dist/{table-DEdW_fDj.js.map → table-Col_FRvb.js.map} +1 -1
  28. package/dist/theme/tailwind.css +373 -0
  29. package/dist/theme.js +123 -0
  30. package/dist/theme.js.map +1 -0
  31. package/dist/types/components/lasso-tool/styles.d.ts +4 -4
  32. package/dist/types/theme/create-carto-theme.d.ts +7 -0
  33. package/dist/types/theme/deep-merge.d.ts +6 -0
  34. package/dist/types/theme/generate-tailwind-css.d.ts +8 -0
  35. package/dist/types/theme/index.d.ts +2 -0
  36. package/dist/types/widgets/actions/index.d.ts +1 -1
  37. package/dist/types/widgets/actions/relative-data/index.d.ts +1 -0
  38. package/dist/types/widgets/actions/relative-data/labels.d.ts +3 -0
  39. package/dist/types/widgets/actions/relative-data/relative-data.d.ts +16 -2
  40. package/dist/types/widgets/actions/relative-data/scale.d.ts +26 -0
  41. package/dist/types/widgets/actions/relative-data/style.d.ts +12 -0
  42. package/dist/types/widgets/category/category.d.ts +2 -1
  43. package/dist/types/widgets/index.d.ts +1 -1
  44. package/dist/types/widgets/markdown/style.d.ts +74 -0
  45. package/dist/types/widgets/utils/spacing-px.d.ts +13 -0
  46. package/dist/{utils-C-eWwOhE.js → utils-Dw4y8tiq.js} +2 -2
  47. package/dist/{utils-C-eWwOhE.js.map → utils-Dw4y8tiq.js.map} +1 -1
  48. package/dist/widgets/actions.js +3 -3
  49. package/dist/widgets/bar.js +85 -84
  50. package/dist/widgets/bar.js.map +1 -1
  51. package/dist/widgets/category.js +1 -1
  52. package/dist/widgets/formula.js +1 -1
  53. package/dist/widgets/histogram.js +109 -108
  54. package/dist/widgets/histogram.js.map +1 -1
  55. package/dist/widgets/markdown.js +1 -1
  56. package/dist/widgets/pie.js +78 -78
  57. package/dist/widgets/pie.js.map +1 -1
  58. package/dist/widgets/scatterplot.js +121 -120
  59. package/dist/widgets/scatterplot.js.map +1 -1
  60. package/dist/widgets/spread.js +1 -1
  61. package/dist/widgets/table.js +1 -1
  62. package/dist/widgets/timeseries.js +91 -90
  63. package/dist/widgets/timeseries.js.map +1 -1
  64. package/dist/widgets/utils.js +1 -1
  65. package/dist/widgets.js +283 -279
  66. package/dist/widgets.js.map +1 -1
  67. package/dist/{zoom-toggle-MmLjMTEw.js → zoom-toggle-DqrV_0Ji.js} +2 -2
  68. package/dist/{zoom-toggle-MmLjMTEw.js.map → zoom-toggle-DqrV_0Ji.js.map} +1 -1
  69. package/package.json +8 -1
  70. package/src/chat/bubbles/styles.ts +2 -0
  71. package/src/components/lasso-tool/lasso-tool-inline.test.tsx +4 -4
  72. package/src/components/lasso-tool/lasso-tool-inline.tsx +6 -6
  73. package/src/components/lasso-tool/styles.ts +4 -4
  74. package/src/theme/create-carto-theme.test.ts +53 -0
  75. package/src/theme/create-carto-theme.ts +21 -0
  76. package/src/theme/deep-merge.test.ts +25 -0
  77. package/src/theme/deep-merge.ts +22 -0
  78. package/src/theme/generate-tailwind-css.test.ts +87 -0
  79. package/src/theme/generate-tailwind-css.ts +178 -0
  80. package/src/theme/index.ts +2 -0
  81. package/src/theme/tailwind-compile.test.ts +71 -0
  82. package/src/theme/tailwind.css +373 -0
  83. package/src/widgets/actions/index.ts +1 -0
  84. package/src/widgets/actions/relative-data/index.ts +1 -0
  85. package/src/widgets/actions/relative-data/labels.ts +6 -0
  86. package/src/widgets/actions/relative-data/relative-data.test.tsx +78 -1
  87. package/src/widgets/actions/relative-data/relative-data.tsx +137 -20
  88. package/src/widgets/actions/relative-data/scale.ts +48 -0
  89. package/src/widgets/actions/relative-data/style.ts +10 -0
  90. package/src/widgets/bar/options.test.ts +16 -0
  91. package/src/widgets/bar/options.ts +13 -8
  92. package/src/widgets/category/category.relative-data.test.tsx +51 -63
  93. package/src/widgets/category/category.tsx +25 -11
  94. package/src/widgets/histogram/options.ts +13 -8
  95. package/src/widgets/index.ts +1 -0
  96. package/src/widgets/markdown/style.ts +40 -0
  97. package/src/widgets/note/widget-note.tsx +4 -1
  98. package/src/widgets/pie/options.ts +5 -4
  99. package/src/widgets/scatterplot/options.test.ts +12 -0
  100. package/src/widgets/scatterplot/options.ts +25 -10
  101. package/src/widgets/timeseries/options.ts +13 -8
  102. package/src/widgets/utils/css-variables-theme.test.ts +32 -0
  103. package/src/widgets/utils/option-builders.ts +4 -6
  104. package/src/widgets/utils/spacing-px.test.ts +42 -0
  105. package/src/widgets/utils/spacing-px.ts +24 -0
  106. package/dist/actions-CkxKe8-J.js +0 -1160
  107. package/dist/actions-CkxKe8-J.js.map +0 -1
  108. package/dist/category-DULnoxvk.js.map +0 -1
  109. package/dist/components-CTUpdQuj.js.map +0 -1
  110. package/dist/item-color-B7xkIpDZ.js.map +0 -1
  111. package/dist/markdown-DmvLi3vx.js +0 -159
  112. package/dist/markdown-DmvLi3vx.js.map +0 -1
  113. package/dist/styles-BCEBm7xh.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"zoom-toggle-MmLjMTEw.js","names":["cfg","Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>","_c","ZoomInIcon","Tooltip","Tooltip$1","useWidgetId","getEchartInstance","useSingleTransform","SLIDER_HANDLE_ICON","SLIDER_HEIGHT","SLIDER_BOTTOM","createAddZoom","axes","options","includeX","includeY","baseSliderProps","getZoomSliderStyles","theme","option","cfg","dataZoom","addZoom","secondary","ZOOM_LAYOUT","DEFAULT_ZOOM_TOGGLE_LABELS","styles","ZOOM_DESCRIPTOR","ZoomToggle","$","c","t1","t2","t0","initialEnabled","Icon","ZoomIn","id","useTheme","t3","labels","_labels","t4","t5","t6","resolved","transform","t7","t8","enabled","toggle","handleToggle","tooltip","t9","t10","t11","t12","iconProps","t13","IconButton","t14"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"zoom-toggle-DqrV_0Ji.js","names":["cfg","Icon = ZoomInIcon","{\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps","initialEnabled = false","{ ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }","axes ? axes.includes('x') : true","axes ? axes.includes('y') : false","createAddZoom(theme, { axes: resolved })","useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])","{ initialEnabled }","() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }","useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])","{ ...styles.toggle, ...(enabled && styles.toggleActive) }","enabled && styles.toggleActive","<Icon fontSize='small' {...iconProps} />","<IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>","enabled ? 'active' : undefined","<Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>","_c","ZoomInIcon","Tooltip","Tooltip$1","useWidgetId","getEchartInstance","useSingleTransform","SLIDER_HANDLE_ICON","SLIDER_HEIGHT","SLIDER_BOTTOM","createAddZoom","axes","options","includeX","includeY","baseSliderProps","getZoomSliderStyles","theme","option","cfg","dataZoom","addZoom","secondary","ZOOM_LAYOUT","DEFAULT_ZOOM_TOGGLE_LABELS","styles","ZOOM_DESCRIPTOR","ZoomToggle","$","c","t1","t2","t0","initialEnabled","Icon","ZoomIn","id","useTheme","t3","labels","_labels","t4","t5","t6","resolved","transform","t7","t8","enabled","toggle","handleToggle","tooltip","t9","t10","t11","t12","iconProps","t13","IconButton","t14"],"sources":["../src/widgets/actions/zoom-toggle/transforms.ts","../src/widgets/actions/zoom-toggle/labels.ts","../src/widgets/actions/zoom-toggle/style.ts","../src/widgets/actions/zoom-toggle/zoom-toggle.tsx"],"sourcesContent":["import type { Theme } from '@mui/material'\n\n/**\n * v1's slider handle — 9×36 SVG with a white pill body, secondary-colored\n * stroke, and three grip lines. Inlined as a data URL so the chart can\n * render it without a network fetch.\n */\nconst SLIDER_HANDLE_ICON =\n 'image://data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOSIgaGVpZ2h0PSIzNiIgdmlld0JveD0iMCAwIDkgMzYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9IjAuNSIgeT0iLTAuNSIgd2lkdGg9IjgiIGhlaWdodD0iMTgiIHJ4PSI0IiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAwIDI3KSIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzM1OEJFNyIvPgo8cmVjdCB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgdHJhbnNmb3JtPSJtYXRyaXgoMSAwIDAgLTEgMyAyMykiIGZpbGw9IiMzNThCRTciLz4KPHJlY3Qgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIHRyYW5zZm9ybT0ibWF0cml4KDEgMCAwIC0xIDMgMTkpIiBmaWxsPSIjMzU4QkU3Ii8+CjxyZWN0IHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiB0cmFuc2Zvcm09Im1hdHJpeCgxIDAgMCAtMSAzIDE1KSIgZmlsbD0iIzM1OEJFNyIvPgo8L3N2Zz4='\n\nconst SLIDER_HEIGHT = 32\nconst SLIDER_BOTTOM = 0\n\n/** Which chart axis (or axes) zoom applies to. */\nexport type ZoomAxis = 'x' | 'y'\n\nexport interface CreateAddZoomOptions {\n /**\n * Axes to enable zoom on. Default `['x']` (matches bar / histogram /\n * timeseries — horizontal pan/zoom on the category axis). Pass\n * `['x', 'y']` for a scatterplot-style 2D zoom (mouse-wheel scales both\n * axes; a horizontal slider sits at the bottom for x-range and a\n * vertical slider sits on the right for y-range).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Theme-bound zoom config transform: adds one inside `dataZoom`\n * (mouse-wheel + drag) per axis plus a styled slider per axis with the\n * v1 chrome (blue-tinted filler, secondary-color data preview, white\n * pill handles, CARTO typography). The x-slider is horizontal at the\n * bottom; the y-slider is vertical on the right.\n *\n * Returned function has stable identity for a given theme + axes\n * combination; pair with `useMemo` at the call site.\n *\n * **No `start` / `end`** are emitted: ECharts defaults to `0–100` on the\n * first `setOption` and then keeps the runtime slider state across\n * subsequent merges (no `replaceMerge` for `dataZoom`). Re-running the\n * transform on data/formatter/RelativeData updates therefore preserves the\n * user's dragged range instead of snapping it back.\n */\nexport function createAddZoom(\n theme: Theme,\n options: CreateAddZoomOptions = {},\n): (option: unknown) => unknown {\n const axes = options.axes ?? ['x']\n const includeX = axes.includes('x')\n const includeY = axes.includes('y')\n const sliderStyles = getZoomSliderStyles(theme)\n const baseSliderProps = {\n throttle: 0,\n showDetail: false,\n brushSelect: false,\n moveHandleSize: 8,\n handleSize: '100%',\n ...sliderStyles,\n }\n return (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n const dataZoom: object[] = []\n if (includeX) {\n dataZoom.push({ type: 'inside', xAxisIndex: [0], throttle: 0 })\n }\n if (includeY) {\n dataZoom.push({ type: 'inside', yAxisIndex: [0], throttle: 0 })\n }\n if (includeX) {\n dataZoom.push({\n type: 'slider',\n xAxisIndex: [0],\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n handleIcon: SLIDER_HANDLE_ICON,\n ...baseSliderProps,\n })\n }\n if (includeY) {\n // Vertical slider — `width` is its thickness, `right` pins it to\n // the right edge. We omit `handleIcon` so ECharts renders its\n // default vertical handle (the inline SVG is shaped for a\n // horizontal slider and looks wrong when rotated).\n dataZoom.push({\n type: 'slider',\n yAxisIndex: [0],\n width: SLIDER_HEIGHT,\n right: SLIDER_BOTTOM,\n ...baseSliderProps,\n })\n }\n return { ...cfg, dataZoom }\n }\n}\n\n/**\n * Back-compat untyped transform — produces the bare `dataZoom` config from\n * the original v2 implementation. Use {@link createAddZoom} when a theme is\n * available so the slider matches the v1 styling.\n *\n * `start` / `end` are intentionally omitted so ECharts preserves the user's\n * runtime range across subsequent `setOption` merges (see {@link createAddZoom}).\n */\nexport const addZoom = (option: unknown): unknown => {\n if (option == null || typeof option !== 'object') return option\n const cfg = option as Record<string, unknown>\n return {\n ...cfg,\n dataZoom: [\n { type: 'inside' },\n {\n type: 'slider',\n height: SLIDER_HEIGHT,\n bottom: SLIDER_BOTTOM,\n },\n ],\n }\n}\n\n/** Style block matching v1's `getEChartZoomSliderStyles`. */\nfunction getZoomSliderStyles(theme: Theme) {\n const secondary = theme.palette.secondary?.main ?? '#358BE7'\n return {\n fillerColor: 'rgba(53, 139, 231, 0.25)',\n borderColor: 'rgba(53, 139, 231, 0.3)',\n borderWidth: 0.5,\n backgroundColor: 'transparent',\n borderRadius: 4,\n dataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n selectedDataBackground: {\n lineStyle: { opacity: 0 },\n areaStyle: { opacity: 1, color: secondary },\n },\n handleStyle: {\n color: theme.palette.common?.white ?? '#fff',\n borderColor: 'rgba(3, 111, 226, 0.08)',\n borderWidth: 1,\n shadowBlur: 3,\n shadowColor: 'rgba(0, 0, 0, 0.1)',\n shadowOffsetX: 0,\n shadowOffsetY: 1,\n },\n textStyle: {\n color: theme.palette.black?.[60] ?? theme.palette.text?.primary,\n fontSize: parseInt(\n (theme.typography.overlineDelicate?.fontSize as string | undefined) ??\n '11',\n ),\n fontFamily:\n theme.typography.overlineDelicate?.fontFamily ??\n theme.typography.caption?.fontFamily,\n },\n }\n}\n\n/**\n * Visual layout constants used by the data-fusion mergers (bar, histogram,\n * timeseries) when laying out the slider against the legend / grid.\n */\nexport const ZOOM_LAYOUT = {\n /** Slider height in px (matches `createAddZoom` output). */\n sliderHeight: SLIDER_HEIGHT,\n /** Vertical gap between chart grid and the slider. */\n sliderGap: 8,\n /** Slider `bottom` when a legend is shown — sits above the legend row. */\n sliderBottomWithLegend: 28,\n} as const\n","export interface ZoomToggleLabels {\n on: string\n off: string\n}\n\nexport const DEFAULT_ZOOM_TOGGLE_LABELS: ZoomToggleLabels = {\n on: 'Disable zoom',\n off: 'Enable zoom',\n}\n","import type { SxProps, Theme } from '@mui/material'\n\nexport const styles = {\n toggle: {\n p: 0.5,\n '& .MuiSvgIcon-root': { fontSize: 20 },\n },\n toggleActive: {\n background: (theme: Theme) => theme.palette.primary.relatedLight,\n },\n} satisfies Record<string, SxProps<Theme>>\n","import { useCallback, useMemo, type ComponentType } from 'react'\nimport { IconButton, useTheme, type SvgIconProps } from '@mui/material'\nimport { ZoomIn as ZoomInIcon } from '@mui/icons-material'\nimport { Tooltip } from '../../../components'\nimport {\n getEchartInstance,\n useSingleTransform,\n useWidgetId,\n} from '../../stores'\nimport { createAddZoom, type ZoomAxis } from './transforms'\nimport { DEFAULT_ZOOM_TOGGLE_LABELS, type ZoomToggleLabels } from './labels'\nimport { styles } from './style'\n\nconst ZOOM_DESCRIPTOR = {\n id: 'zoom-toggle',\n type: 'config' as const,\n order: 20,\n}\n\nexport interface ZoomToggleProps {\n initialEnabled?: boolean\n labels?: Partial<ZoomToggleLabels>\n icon?: ComponentType<SvgIconProps>\n iconProps?: SvgIconProps\n /**\n * Which axes the zoom controls. Default `['x']` — matches bar /\n * histogram / timeseries (horizontal pan on the category axis). Pass\n * `['x', 'y']` for 2D zoom on a scatterplot-style chart (mouse-wheel\n * scales both axes; horizontal slider at bottom + vertical slider on\n * the right).\n */\n axes?: readonly ZoomAxis[]\n}\n\n/**\n * Toggle a `dataZoom` slider + wheel-zoom on the chart. The transform is\n * theme-bound so the slider gets v1's CARTO chrome (blue-tinted filler,\n * secondary-color preview, white pill handles).\n *\n * `dataZoom` is intentionally NOT in `replaceMergeKeys` — ECharts merges\n * subsequent `setOption` calls so the user's dragged range survives\n * unrelated re-renders (data updates, formatter swaps, RelativeData toggles).\n *\n * On disable the transform is unregistered, but ECharts' merge semantics\n * would otherwise *keep* the live `dataZoom` component visible. To remove\n * the slider immediately we issue a one-shot\n * `setOption({}, { replaceMerge: ['dataZoom'] })` against the live chart\n * instance via {@link getEchartInstance} — this bypasses the pipeline so\n * unrelated transforms aren't disturbed.\n */\nexport function ZoomToggle({\n initialEnabled = false,\n labels,\n icon: Icon = ZoomInIcon,\n iconProps,\n axes,\n}: ZoomToggleProps) {\n const id = useWidgetId()\n const theme = useTheme()\n const _labels = { ...DEFAULT_ZOOM_TOGGLE_LABELS, ...labels }\n // Reduce `axes` to two structural booleans before memoizing so the memo\n // depends on stable primitive values, not an array reference (callers\n // typically pass an inline `axes={['x', 'y']}` literal that changes\n // identity each render). Default — no `axes` prop — is x-axis only,\n // matching `createAddZoom`'s default.\n const includeX = axes ? axes.includes('x') : true\n const includeY = axes ? axes.includes('y') : false\n const transform = useMemo(() => {\n const resolved: ZoomAxis[] = []\n if (includeX) resolved.push('x')\n if (includeY) resolved.push('y')\n return createAddZoom(theme, { axes: resolved })\n }, [theme, includeX, includeY])\n const { enabled, toggle } = useSingleTransform(\n id,\n ZOOM_DESCRIPTOR,\n transform,\n { initialEnabled },\n )\n\n const handleToggle = useCallback(() => {\n if (enabled) {\n // About to disable — clear the dataZoom component on the live\n // instance so the slider disappears immediately.\n getEchartInstance(id)?.setOption({}, { replaceMerge: ['dataZoom'] })\n }\n toggle()\n }, [enabled, toggle, id])\n\n const tooltip = enabled ? _labels.on : _labels.off\n\n return (\n <Tooltip title={tooltip}>\n <IconButton\n size='small'\n aria-label={tooltip}\n aria-pressed={enabled}\n onClick={handleToggle}\n className={enabled ? 'active' : undefined}\n sx={{ ...styles.toggle, ...(enabled && styles.toggleActive) }}\n >\n <Icon fontSize='small' {...iconProps} />\n </IconButton>\n </Tooltip>\n )\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/ps-react-ui",
3
- "version": "6.3.0",
3
+ "version": "6.5.0",
4
4
  "description": "CARTO's Professional Service React Material library",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -19,6 +19,7 @@
19
19
  "jscodeshift": "^17.4.0",
20
20
  "react-dom": "19.2.8",
21
21
  "react-markdown": "10.1.0",
22
+ "tailwindcss": "^4.3.3",
22
23
  "zustand": "5.0.15",
23
24
  "@carto/ps-common-types": "1.0.0",
24
25
  "@carto/ps-utils": "2.2.0"
@@ -56,6 +57,11 @@
56
57
  "import": "./dist/components.js",
57
58
  "types": "./dist/types/components/index.d.ts"
58
59
  },
60
+ "./theme": {
61
+ "import": "./dist/theme.js",
62
+ "types": "./dist/types/theme/index.d.ts"
63
+ },
64
+ "./theme/tailwind.css": "./dist/theme/tailwind.css",
59
65
  "./legend": {
60
66
  "import": "./dist/legend.js",
61
67
  "types": "./dist/types/legend/index.d.ts"
@@ -165,6 +171,7 @@
165
171
  "dev": "vite build --watch --emptyOutDir=false",
166
172
  "build": "vite build",
167
173
  "exports:validate": "node --experimental-strip-types scripts/generate-widget-config.ts --validate",
174
+ "tailwind:generate": "vitest run src/theme/generate-tailwind-css.test.ts -u --maxWorkers=25%",
168
175
  "lint": "eslint",
169
176
  "typecheck": "tsc --noEmit",
170
177
  "test": "vitest run",
@@ -7,6 +7,7 @@ import {
7
7
  } from '@mui/material'
8
8
  import type { CSSObject } from '@mui/material/styles'
9
9
  import type { FC } from 'react'
10
+ import { markdownElementDefaults } from '../../widgets/markdown/style'
10
11
 
11
12
  // The explicit annotation is what keeps this file's .d.ts emittable: the type
12
13
  // `styled()` infers can only be named through a pnpm-internal @mui/system path
@@ -30,6 +31,7 @@ ChatMessageOverflow.displayName = 'ChatMessageOverflow'
30
31
  export const styles = {
31
32
  agentMessageContainer: (theme: Theme): CSSObject => ({
32
33
  ...theme.typography.body2,
34
+ ...markdownElementDefaults,
33
35
  width: '100%',
34
36
  display: 'flex',
35
37
  flexDirection: 'column',
@@ -212,7 +212,7 @@ describe('LassoToolsInlineUI branches', () => {
212
212
  />,
213
213
  )
214
214
  // The "no data" Typography branch should have rendered
215
- expect(container.querySelector('.inline')).toBeDefined()
215
+ expect(container.querySelector('.PsLassoTools-inline')).toBeDefined()
216
216
  })
217
217
 
218
218
  test('renders without onChangeMode prop (optional callback)', async () => {
@@ -604,7 +604,7 @@ describe('LassoToolsInlineUI toolbar-level actions', () => {
604
604
  })
605
605
 
606
606
  test('separates toolbar blocks with CSS borders, not top-level MuiDividers', () => {
607
- // Horizontal rules between stacked blocks come from `.inline > * + *`
607
+ // Horizontal rules between stacked blocks come from `.PsLassoTools-inline > * + *`
608
608
  // (styles.ts). Blocks must not emit top-level `<Divider flexItem />` —
609
609
  // those would double up when a middle block is omitted via children.
610
610
  const { container } = render(
@@ -627,8 +627,8 @@ describe('LassoToolsInlineUI toolbar-level actions', () => {
627
627
  )
628
628
 
629
629
  const root = container.firstElementChild!
630
- expect(root.classList.contains('inline')).toBe(true)
631
- // Provider is context-only: block roots are direct children of `.inline`.
630
+ expect(root.classList.contains('PsLassoTools-inline')).toBe(true)
631
+ // Provider is context-only: block roots are direct children of `.PsLassoTools-inline`.
632
632
  expect(root.children.length).toBeGreaterThan(1)
633
633
  expect(root.querySelectorAll(':scope > .MuiDivider-root')).toHaveLength(0)
634
634
  })
@@ -97,7 +97,7 @@ export function LassoToolsInlineUI({
97
97
  // Array merge: `sx` can be an object, a function or an array.
98
98
  sx={mergeSx(styles.container, sx)}
99
99
  {...BoxProps}
100
- className='inline'
100
+ className='PsLassoTools-inline'
101
101
  >
102
102
  <LassoToolsUIProvider value={contextValue}>
103
103
  {children}
@@ -134,7 +134,7 @@ function InteractionModeWrapperBase({
134
134
  {showToggles && (
135
135
  <Typography
136
136
  variant='subtitle2'
137
- className='inline'
137
+ className='PsLassoTools-inline'
138
138
  sx={[
139
139
  {
140
140
  color: 'text.secondary',
@@ -218,7 +218,7 @@ function ModesWrapperBase({
218
218
  <Box>
219
219
  <Typography
220
220
  variant='subtitle2'
221
- className='inline'
221
+ className='PsLassoTools-inline'
222
222
  sx={[
223
223
  {
224
224
  color: 'text.secondary',
@@ -238,7 +238,7 @@ function ModesWrapperBase({
238
238
  // controlled mode across renders (e.g. toggling `enabled`).
239
239
  value={modeSelectedValue?.value ?? null}
240
240
  exclusive
241
- className='inline'
241
+ className='PsLassoTools-inline'
242
242
  sx={styles.options.menu}
243
243
  onChange={handleClick}
244
244
  >
@@ -305,7 +305,7 @@ function ChipsWrapperBase({
305
305
  <Box>
306
306
  <NoData hasValues={!!chipValues.length} labels={labels?.noData} />
307
307
  {!!chipValues.length && (
308
- <Box sx={styles.chip.container} className='inline'>
308
+ <Box sx={styles.chip.container} className='PsLassoTools-inline'>
309
309
  {chipValues.map((value) => (
310
310
  <LassoToolsUIChip
311
311
  key={value.id}
@@ -430,7 +430,7 @@ function InteractionModeToggles({
430
430
  <ToggleButtonGroup
431
431
  value={value}
432
432
  exclusive
433
- className='inline'
433
+ className='PsLassoTools-inline'
434
434
  sx={styles.options.menu}
435
435
  onChange={(_evt, mode: LassoGeometryType | null) => {
436
436
  if (mode) {
@@ -39,7 +39,7 @@ export const styles = {
39
39
  overflow: 'hidden',
40
40
  width: 'fit-content',
41
41
 
42
- '&.inline': {
42
+ '&.PsLassoTools-inline': {
43
43
  flexDirection: 'column',
44
44
  alignItems: 'flex-start',
45
45
  gap: ({ spacing }) => spacing(1),
@@ -74,7 +74,7 @@ export const styles = {
74
74
  menu: {
75
75
  '&.MuiMenu-list': { maxHeight: MENU_MAX_HEIGHT },
76
76
  paddingY: ({ spacing }) => spacing(1),
77
- '&.inline': {
77
+ '&.PsLassoTools-inline': {
78
78
  boxShadow: 'none',
79
79
  backgroundColor: 'transparent',
80
80
  borderRadius: ({ spacing }) => spacing(0.5),
@@ -145,7 +145,7 @@ export const styles = {
145
145
  paddingX: ({ spacing }) => spacing(2),
146
146
  paddingBottom: ({ spacing }) => spacing(0.5),
147
147
 
148
- '&.inline': {
148
+ '&.PsLassoTools-inline': {
149
149
  paddingX: 0,
150
150
  paddingBottom: 0,
151
151
  },
@@ -175,7 +175,7 @@ export const styles = {
175
175
  scrollbarWidth: 'none',
176
176
  paddingX: ({ spacing }) => spacing(1),
177
177
 
178
- '&.inline': {
178
+ '&.PsLassoTools-inline': {
179
179
  overflowX: 'visible',
180
180
  flexWrap: 'wrap',
181
181
  },
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ cartoThemeOptions,
4
+ theme as cartoTheme,
5
+ } from '@carto/meridian-ds/theme'
6
+ import { createCartoTheme } from './create-carto-theme'
7
+
8
+ type StyleSheets = Record<string, Record<string, string>>[]
9
+ const sheets = (t: unknown) =>
10
+ (t as { generateStyleSheets: () => StyleSheets }).generateStyleSheets()
11
+ const rootVars = (t: unknown): Record<string, string> =>
12
+ Object.assign({}, ...sheets(t).map((s) => Object.values(s)[0])) as Record<
13
+ string,
14
+ string
15
+ >
16
+
17
+ describe('createCartoTheme', () => {
18
+ it('enables cssVariables and emits CARTO extension variables', () => {
19
+ const vars = rootVars(createCartoTheme())
20
+ expect(vars['--mui-palette-primary-main']).toBe(
21
+ cartoTheme.palette.primary.main,
22
+ )
23
+ expect(vars['--mui-palette-brand-navyBlue']).toBe(
24
+ cartoTheme.palette.brand.navyBlue,
25
+ )
26
+ expect(vars['--mui-palette-qualitative-bold-0']).toBeDefined()
27
+ })
28
+
29
+ it('keeps responsive font sizes like the meridian theme', () => {
30
+ expect(createCartoTheme().typography.h1).toEqual(cartoTheme.typography.h1)
31
+ })
32
+
33
+ it('merges overrides without dropping CARTO palette extensions', () => {
34
+ const theme = createCartoTheme({
35
+ palette: { primary: { main: '#ff0000' } },
36
+ })
37
+ const vars = rootVars(theme)
38
+ expect(vars['--mui-palette-primary-main']).toBe('#ff0000')
39
+ expect(vars['--mui-palette-tertiary-main']).toBe(
40
+ cartoTheme.palette.tertiary.main,
41
+ )
42
+ expect(theme.palette.brand.navyBlue).toBe(cartoTheme.palette.brand.navyBlue)
43
+ })
44
+
45
+ it('does not mutate meridian options between calls', () => {
46
+ const before = JSON.stringify(cartoThemeOptions)
47
+ createCartoTheme({ palette: { primary: { main: '#ff0000' } } })
48
+ expect(JSON.stringify(cartoThemeOptions)).toBe(before)
49
+ expect(rootVars(createCartoTheme())['--mui-palette-primary-main']).toBe(
50
+ cartoTheme.palette.primary.main,
51
+ )
52
+ })
53
+ })
@@ -0,0 +1,21 @@
1
+ import {
2
+ createTheme,
3
+ responsiveFontSizes,
4
+ type Theme,
5
+ type ThemeOptions,
6
+ } from '@mui/material'
7
+ import { cartoThemeOptions } from '@carto/meridian-ds/theme'
8
+ import { deepMerge } from './deep-merge'
9
+
10
+ /**
11
+ * The CARTO theme with MUI CSS variables enabled, so CSS (Tailwind's
12
+ * `@theme inline`, plain stylesheets) can read `var(--mui-*)`. Same tokens
13
+ * and responsive font sizes as meridian's `theme`.
14
+ */
15
+ export function createCartoTheme(overrides?: ThemeOptions): Theme {
16
+ const options = deepMerge<ThemeOptions>(
17
+ { ...cartoThemeOptions, cssVariables: true },
18
+ overrides,
19
+ )
20
+ return responsiveFontSizes(createTheme(options))
21
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { deepMerge } from './deep-merge'
3
+
4
+ describe('deepMerge', () => {
5
+ it('merges nested plain objects', () => {
6
+ expect(
7
+ deepMerge({ a: { b: 1, c: 2 }, d: 1 }, { a: { c: 3 }, e: 4 }),
8
+ ).toEqual({ a: { b: 1, c: 3 }, d: 1, e: 4 })
9
+ })
10
+
11
+ it('replaces arrays and non-plain values instead of merging them', () => {
12
+ const fn = () => 1
13
+ expect(deepMerge({ a: [1, 2], f: { x: 1 } }, { a: [3], f: fn })).toEqual({
14
+ a: [3],
15
+ f: fn,
16
+ })
17
+ })
18
+
19
+ it('ignores undefined overrides and does not mutate inputs', () => {
20
+ const base = { a: { b: 1 } }
21
+ const out = deepMerge(base, { a: { b: undefined, c: 2 } })
22
+ expect(out).toEqual({ a: { b: 1, c: 2 } })
23
+ expect(base).toEqual({ a: { b: 1 } })
24
+ })
25
+ })
@@ -0,0 +1,22 @@
1
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
2
+ if (value === null || typeof value !== 'object') return false
3
+ const proto = Object.getPrototypeOf(value) as unknown
4
+ return proto === Object.prototype || proto === null
5
+ }
6
+
7
+ /**
8
+ * Deep-merges `override` into a copy of `base`. Plain objects merge key by
9
+ * key; arrays, functions and other values replace. `undefined` in the
10
+ * override keeps the base value. Inputs are not mutated.
11
+ */
12
+ export function deepMerge<T>(base: T, override: unknown): T {
13
+ if (!isPlainObject(base) || !isPlainObject(override)) {
14
+ return (override === undefined ? base : override) as T
15
+ }
16
+ const out: Record<string, unknown> = { ...base }
17
+ for (const [key, value] of Object.entries(override)) {
18
+ if (value === undefined) continue
19
+ out[key] = deepMerge(base[key], value)
20
+ }
21
+ return out as T
22
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createCartoTheme } from './create-carto-theme'
3
+ import { generateTailwindCss } from './generate-tailwind-css'
4
+
5
+ const theme = createCartoTheme()
6
+ const css = generateTailwindCss(theme)
7
+
8
+ type StyleSheets = Record<string, Record<string, string>>[]
9
+ const definedVars = new Set(
10
+ (theme as unknown as { generateStyleSheets: () => StyleSheets })
11
+ .generateStyleSheets()
12
+ .flatMap((sheet) => Object.keys(Object.values(sheet)[0] ?? {})),
13
+ )
14
+
15
+ describe('generateTailwindCss', () => {
16
+ it('matches the committed tailwind.css (run `pnpm tailwind:generate` to update)', async () => {
17
+ await expect(css).toMatchFileSnapshot('./tailwind.css')
18
+ })
19
+
20
+ it('only references --mui-* variables the theme defines', () => {
21
+ const used = [...css.matchAll(/var\((--mui-[\w-]+)\)/g)].map(
22
+ (m) => m[1] ?? '',
23
+ )
24
+ expect(used.length).toBeGreaterThan(100)
25
+ expect(used.filter((v) => !definedVars.has(v))).toEqual([])
26
+ })
27
+
28
+ it('maps palette colours, CARTO extensions, shadows and radius', () => {
29
+ expect(css).toContain('--color-primary: var(--mui-palette-primary-main);')
30
+ expect(css).toContain(
31
+ '--color-primary-contrast: var(--mui-palette-primary-contrastText);',
32
+ )
33
+ expect(css).toContain(
34
+ '--color-tertiary-related-light: var(--mui-palette-tertiary-relatedLight);',
35
+ )
36
+ expect(css).toContain(
37
+ '--color-brand-navy-blue: var(--mui-palette-brand-navyBlue);',
38
+ )
39
+ expect(css).toContain('--color-black-12: var(--mui-palette-black-12);')
40
+ expect(css).toContain(
41
+ '--color-qualitative-3: var(--mui-palette-qualitative-bold-3);',
42
+ )
43
+ expect(css).toContain(
44
+ '--color-text-secondary: var(--mui-palette-text-secondary);',
45
+ )
46
+ expect(css).toContain('--color-paper: var(--mui-palette-background-paper);')
47
+ expect(css).toContain('--shadow-24: var(--mui-shadows-24);')
48
+ expect(css).toContain('--radius-theme: var(--mui-shape-borderRadius);')
49
+ expect(css).toContain(
50
+ '--radius-theme-2: calc(var(--mui-shape-borderRadius) * 2);',
51
+ )
52
+ expect(css).toContain(
53
+ '--radius-theme-half: calc(var(--mui-shape-borderRadius) * 0.5);',
54
+ )
55
+ expect(css).toContain(
56
+ '--color-action-hover: var(--mui-palette-action-hover);',
57
+ )
58
+ expect(css).toContain(
59
+ '--color-common-white: var(--mui-palette-common-white);',
60
+ )
61
+ expect(css).not.toMatch(
62
+ /Channel|--color-common-(?!white|black)|--color-alert/i,
63
+ )
64
+ })
65
+
66
+ it('writes breakpoints as literals from the theme and drops 2xl', () => {
67
+ for (const [key, px] of Object.entries(theme.breakpoints.values)) {
68
+ expect(css).toContain(`--breakpoint-${key}: ${px}px;`)
69
+ }
70
+ expect(css).toContain('--breakpoint-2xl: initial;')
71
+ })
72
+
73
+ it('emits responsive typography utilities with letter spacing', () => {
74
+ expect(css).toMatch(/@utility typography-h1 \{[^}]*font-size: 3\.5rem;/)
75
+ expect(css).toMatch(
76
+ /@utility typography-h1 \{[^}]*letter-spacing: -1\.5px;/,
77
+ )
78
+ expect(css).toContain('@media (min-width:1280px) {\n font-size: 6rem;')
79
+ expect(css).toContain('@utility typography-caption-medium {')
80
+ expect(css).toContain('@utility typography-code1 {')
81
+ expect(css).not.toContain('typography-inherit')
82
+ })
83
+
84
+ it('does not declare the layer order', () => {
85
+ expect(css).not.toContain('@layer')
86
+ })
87
+ })
@@ -0,0 +1,178 @@
1
+ import type { Theme } from '@mui/material'
2
+
3
+ const PALETTE_COLORS = [
4
+ 'primary',
5
+ 'secondary',
6
+ 'tertiary',
7
+ 'error',
8
+ 'warning',
9
+ 'info',
10
+ 'success',
11
+ 'neutral',
12
+ 'default',
13
+ ] as const
14
+
15
+ const COLOR_KEYS: Record<string, string> = {
16
+ main: '',
17
+ light: '-light',
18
+ dark: '-dark',
19
+ contrastText: '-contrast',
20
+ relatedLight: '-related-light',
21
+ relatedDark: '-related-dark',
22
+ background: '-background',
23
+ }
24
+
25
+ const HEADER = `/*
26
+ * Generated by generateTailwindCss() from @carto/ps-react-ui/theme.
27
+ * Do not edit by hand: regenerate it whenever the theme changes.
28
+ *
29
+ * Import after \`tailwindcss\`. Declare the cascade layer order yourself first:
30
+ * theme, base, mui, components, utilities (in that order).
31
+ */`
32
+
33
+ type Dict = Record<string, unknown>
34
+
35
+ /** `navyBlue` → `navy-blue`, `captionMedium` → `caption-medium`, `code1` → `code1`. */
36
+ function kebab(key: string): string {
37
+ return key.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
38
+ }
39
+
40
+ function stringKeys(obj: unknown): string[] {
41
+ if (!obj || typeof obj !== 'object') return []
42
+ return Object.keys(obj).filter((k) => typeof (obj as Dict)[k] === 'string')
43
+ }
44
+
45
+ function colorLines(theme: Theme): string[] {
46
+ const palette = theme.palette as unknown as Dict
47
+ const v = (path: string) => `var(--mui-palette-${path})`
48
+ const lines: string[] = []
49
+
50
+ for (const name of PALETTE_COLORS) {
51
+ const color = palette[name] as Dict | undefined
52
+ for (const [key, suffix] of Object.entries(COLOR_KEYS)) {
53
+ if (typeof color?.[key] === 'string')
54
+ lines.push(`--color-${name}${suffix}: ${v(`${name}-${key}`)};`)
55
+ }
56
+ }
57
+ for (const key of ['primary', 'secondary', 'disabled']) {
58
+ if (typeof (palette.text as Dict | undefined)?.[key] === 'string')
59
+ lines.push(`--color-text-${key}: ${v(`text-${key}`)};`)
60
+ }
61
+ lines.push(`--color-background: ${v('background-default')};`)
62
+ lines.push(`--color-paper: ${v('background-paper')};`)
63
+ lines.push(`--color-divider: ${v('divider')};`)
64
+ // `*Channel` keys are bare RGB triplets for `rgba()`, not colours.
65
+ for (const key of stringKeys(palette.action).filter(
66
+ (k) => !k.endsWith('Channel'),
67
+ ))
68
+ lines.push(`--color-action-${kebab(key)}: ${v(`action-${key}`)};`)
69
+ for (const key of ['white', 'black'])
70
+ lines.push(`--color-common-${key}: ${v(`common-${key}`)};`)
71
+ for (const key of stringKeys(palette.grey))
72
+ lines.push(`--color-grey-${key}: ${v(`grey-${key}`)};`)
73
+ for (const key of stringKeys(palette.brand))
74
+ lines.push(`--color-brand-${kebab(key)}: ${v(`brand-${key}`)};`)
75
+ for (const shade of ['white', 'black']) {
76
+ for (const key of stringKeys(palette[shade]))
77
+ lines.push(`--color-${shade}-${key}: ${v(`${shade}-${key}`)};`)
78
+ }
79
+ const bold = (palette.qualitative as Dict | undefined)?.bold
80
+ for (const key of stringKeys(bold))
81
+ lines.push(`--color-qualitative-${key}: ${v(`qualitative-bold-${key}`)};`)
82
+ return lines
83
+ }
84
+
85
+ // `rounded-theme-2` is `theme.shape.borderRadius * 2`, as in sx.
86
+ const RADIUS_MULTIPLES: [string, number][] = [
87
+ ['half', 0.5],
88
+ ['2', 2],
89
+ ['3', 3],
90
+ ['4', 4],
91
+ ]
92
+
93
+ function themeBlock(theme: Theme): string {
94
+ const lines = [
95
+ ...colorLines(theme),
96
+ ...theme.shadows
97
+ .map((_, n) => `--shadow-${n}: var(--mui-shadows-${n});`)
98
+ .slice(1),
99
+ '--radius-theme: var(--mui-shape-borderRadius);',
100
+ ...RADIUS_MULTIPLES.map(
101
+ ([key, n]) =>
102
+ `--radius-theme-${key}: calc(var(--mui-shape-borderRadius) * ${n});`,
103
+ ),
104
+ ...Object.entries(theme.breakpoints.values).map(
105
+ ([key, px]) => `--breakpoint-${key}: ${px}px;`,
106
+ ),
107
+ '--breakpoint-2xl: initial;',
108
+ ]
109
+ return `@theme inline {\n${lines.map((l) => ` ${l}`).join('\n')}\n}`
110
+ }
111
+
112
+ // Not inline: utilities keep `calc(var(--spacing) * n)`, and `--spacing`
113
+ // follows the theme, so `p-2` is `theme.spacing(1)` (8px by default) even
114
+ // when an app overrides `spacing`.
115
+ const SPACING_BLOCK = `@theme {\n --spacing: calc(var(--mui-spacing) / 2);\n}`
116
+
117
+ function cssValue(prop: string, value: unknown): string {
118
+ if (
119
+ typeof value === 'number' &&
120
+ (prop === 'font-size' || prop === 'letter-spacing')
121
+ )
122
+ return value === 0 ? '0' : `${value}px`
123
+ return String(value)
124
+ }
125
+
126
+ const TYPOGRAPHY_PROPS: [string, string][] = [
127
+ ['fontFamily', 'font-family'],
128
+ ['fontWeight', 'font-weight'],
129
+ ['fontSize', 'font-size'],
130
+ ['lineHeight', 'line-height'],
131
+ ['letterSpacing', 'letter-spacing'],
132
+ ['textTransform', 'text-transform'],
133
+ ]
134
+
135
+ function typographyUtilities(theme: Theme): string[] {
136
+ const typography = theme.typography as unknown as Dict
137
+ return Object.entries(typography)
138
+ .filter(
139
+ ([name, style]) =>
140
+ name !== 'inherit' &&
141
+ !!style &&
142
+ typeof style === 'object' &&
143
+ 'fontSize' in style,
144
+ )
145
+ .map(([name, style]) => {
146
+ const s = style as Dict
147
+ const body = TYPOGRAPHY_PROPS.filter(([key]) => s[key] !== undefined).map(
148
+ ([key, prop]) => ` ${prop}: ${cssValue(prop, s[key])};`,
149
+ )
150
+ for (const [query, rule] of Object.entries(s)) {
151
+ if (!query.startsWith('@media') || !rule || typeof rule !== 'object')
152
+ continue
153
+ const size = (rule as Dict).fontSize
154
+ if (size !== undefined)
155
+ body.push(
156
+ ` ${query} {\n font-size: ${cssValue('font-size', size)};\n }`,
157
+ )
158
+ }
159
+ return `@utility typography-${kebab(name)} {\n${body.join('\n')}\n}`
160
+ })
161
+ }
162
+
163
+ /**
164
+ * Tailwind v4 source CSS for the CARTO theme: colours, shadows and radius
165
+ * read the runtime MUI variables; breakpoints and typography are literals
166
+ * because `@media` cannot read variables and MUI's font variables are not
167
+ * responsive.
168
+ */
169
+ export function generateTailwindCss(theme: Theme): string {
170
+ return (
171
+ [
172
+ HEADER,
173
+ themeBlock(theme),
174
+ SPACING_BLOCK,
175
+ ...typographyUtilities(theme),
176
+ ].join('\n\n') + '\n'
177
+ )
178
+ }
@@ -0,0 +1,2 @@
1
+ export { createCartoTheme } from './create-carto-theme'
2
+ export { generateTailwindCss } from './generate-tailwind-css'