@carto/meridian-ds 5.2.1-alpha.de0b18a.359 → 5.2.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  ## 5.0
6
6
 
7
+ ### 5.2.1
8
+
9
+ - ci(release): automate releases, alphas and release notes [#569](https://github.com/CartoDB/meridian-ds/pull/569)
10
+
7
11
  ### 5.2.0
8
12
 
9
13
  - feat(dialog): export useScrollBorders hook for scroll-edge borders [sc-578988] [#564](https://github.com/CartoDB/meridian-ds/pull/564)
@@ -178,7 +178,12 @@ const NON_ZERO = "Literal[raw=/^[0-9.]+$/][raw!=/^0+(\\.0+)?$/]";
178
178
  const exactly = (...alternatives) => `/^(${alternatives.join("|")})$/`;
179
179
  const SPACING = "[mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap";
180
180
  const SPACING_KEY = exactly(SPACING);
181
- const MERIDIAN_COMPONENT = exactly(...MERIDIAN_COMPONENTS);
181
+ const MERIDIAN_IMPORT = /^@carto\/meridian-ds(\/|$)/;
182
+ const CSS_WIDE = "inherit|initial|unset|revert|revert-layer";
183
+ const PALETTE_VALUE = `/^(${CSS_WIDE}|currentColor|transparent|divider|[a-z]+\\.[A-Za-z0-9]+(\\.[A-Za-z0-9]+)?)$/`;
184
+ const ICON = "/(Icon|Outlined|Rounded|Sharp|TwoTone)$/";
185
+ const VISUAL_KEY = "/^(background|border|border(Top|Right|Bottom|Left)?(Color|Width|Style)?|borderRadius|boxShadow|opacity|outline|textTransform|textDecoration|fill|stroke)$/";
186
+ const COLOR_KEY = "/^(color|bgcolor|backgroundColor)$/";
182
187
  const SX = ":matches(JSXAttribute[name.name='sx'], Property[key.name='sx'])";
183
188
  const STYLED_OR_STYLE = ":matches(CallExpression[callee.callee.name='styled'], CallExpression[callee.object.name='styled'], CallExpression[callee.callee.object.name='styled'], JSXAttribute[name.name='style'])";
184
189
  const SPACING_CALL = "CallExpression[callee.property.name='spacing']";
@@ -193,109 +198,138 @@ const WRAPPERS = [
193
198
  "TemplateLiteral > ConditionalExpression > "
194
199
  ];
195
200
  const under = (parent, nodes) => nodes.flatMap((node) => WRAPPERS.map((w) => `${parent} > ${w}${node}`));
196
- const meridianDesignRules = {
197
- "spacing-step": {
198
- selector: anyOf(
199
- ...under(`${SX} Property[key.name=${SPACING_KEY}]`, [
200
- ...signed(OFF_SCALE),
201
- `ArrayExpression > ${OFF_SCALE}`,
202
- `ObjectExpression > Property > ${OFF_SCALE}`
203
- ]),
204
- ...under(
205
- `Property[key.name=${SPACING_KEY}]`,
206
- signed(OFF_SCALE).map((n) => `${SPACING_CALL} > ${n}`)
207
- )
208
- ),
209
- message: `Spacing off the Meridian scale: margin, padding and gap use ${spacingSteps.SPACING_STEPS.join(", ")}.`
210
- },
211
- "px-spacing": {
212
- selector: anyOf(
213
- ...under(`Property[key.name=${SPACING_KEY}]`, [
214
- PX_STRING,
215
- `ArrayExpression > ${PX_STRING}`,
216
- `ObjectExpression > Property > ${PX_STRING}`
217
- ]),
218
- ...under(
219
- `${STYLED_OR_STYLE} Property[key.name=${SPACING_KEY}]`,
220
- signed(NON_ZERO)
221
- )
222
- ),
223
- message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` are px."
224
- },
225
- "filled-icon": {
226
- selector: anyOf(
227
- "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
228
- "ImportDeclaration[source.value='@mui/icons-material'][importKind!='type'] > ImportSpecifier[importKind!='type'][imported.name!=/(Outlined|Rounded|Sharp|TwoTone)$/]"
229
- ),
230
- message: "Filled MUI icon: import the Outlined variant."
231
- },
232
- "alpha-white-black": {
233
- selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
234
- message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
235
- },
236
- "hex-color": {
237
- selector: STYLE_STRING(
238
- "/(^|[\\s(,])#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b/"
239
- ),
240
- message: "Hardcoded hex color: use a palette token."
241
- },
242
- "rgb-color": {
243
- selector: STYLE_STRING("/(^|[^a-z])rgba?\\(/"),
244
- message: "Hardcoded rgb()/rgba() color: use a palette token."
245
- },
246
- "typography-style": {
247
- selector: anyOf(
248
- ...under(
249
- `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name=/^(fontSize|fontWeight|lineHeight|fontFamily)$/]`,
250
- ["Literal[value!=/^(inherit|initial|unset|revert|revert-layer)$/]"]
251
- )
252
- ),
253
- message: "Hardcoded typography: use `<Typography variant>` (and `weight`), `sx={{ typography: 'body2' }}` or `...theme.typography.body2`."
254
- },
255
- "external-link": {
256
- selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
257
- message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
258
- },
259
- "system-props": {
260
- selector: "JSXOpeningElement[name.name=/^(Box|Stack|Grid|Grid2|Container)$/] > JSXAttribute[name.name=/^(m|p)[trblxy]?$/]",
261
- message: "MUI system props are deprecated: move spacing into `sx`, e.g. `<Box sx={{ px: 2 }}>`."
262
- },
263
- "data-name": {
264
- selector: `JSXOpeningElement[name.name=${MERIDIAN_COMPONENT}] > JSXAttribute[name.name='data-name']`,
265
- message: "`data-name` on a Meridian component overrides its own marker: set it on a wrapper, or drop it."
266
- },
267
- "literal-a11y-text": {
268
- selector: anyOf(
269
- ...["", "JSXExpressionContainer > "].map(
270
- (wrapper) => `JSXAttribute[name.name=/^(aria-label|placeholder)$/] > ${wrapper}Literal[value=/[A-Za-z]/]`
271
- )
272
- ),
273
- message: "Hardcoded `aria-label` / `placeholder`: translate it with the i18n layer, and keep test ids out of it."
274
- },
275
- "raw-shadow-radius": {
276
- selector: anyOf(
277
- "Property[key.name='boxShadow'] > Literal[value=/\\d/]",
278
- "Property[key.name='borderRadius'] > Literal[value=/^\\d+(\\.\\d+)?px$/]"
279
- ),
280
- message: "Hardcoded shadow or radius: use `theme.shadows[n]` / `theme.shape.borderRadius`."
281
- },
282
- "styled-meridian": {
283
- selector: `CallExpression[callee.name='styled'] > Identifier[name=${MERIDIAN_COMPONENT}]`,
284
- message: "`styled()` of a Meridian component restyles it locally: use its props/variants, or raise the gap upstream in Meridian."
285
- },
286
- "visual-sx": {
287
- selector: `JSXOpeningElement[name.name=${MERIDIAN_COMPONENT}] > JSXAttribute[name.name='sx'] Property[key.name=/^(color|bgcolor|background|backgroundColor|border|border(Top|Right|Bottom|Left)?(Color|Width|Style)?|borderRadius|boxShadow|opacity|outline|textTransform|textDecoration|fill|stroke)$/]`,
288
- message: "Visual `sx` on a Meridian component overrides its design: keep `sx` to layout (spacing, size, position) and use its props/variants for the look."
289
- },
290
- "disabled-without-reason": {
291
- selector: "JSXOpeningElement[name.name=/^(Button|IconButton|SplitButton)$/]:has(> JSXAttribute[name.name='disabled']:not([value.expression.value=false])):not(:has(> JSXAttribute[name.name=/^(tooltip|aria-describedby)$/]))",
292
- message: "Disabled without a reason: pass `tooltip` (or `aria-describedby`) saying why it can’t be used."
293
- },
294
- "disabled-cursor": {
295
- selector: "Property[key.name='cursor'] > Literal[value='not-allowed']",
296
- message: "Don't tweak the cursor for disabled states: the component already styles them."
297
- }
298
- };
201
+ const esqueryRegex = (re) => `/${re.source.replace(/\\?\//g, "\\x2F")}/`;
202
+ function createMeridianDesignRules({
203
+ importSource = MERIDIAN_IMPORT
204
+ } = {}) {
205
+ const source = esqueryRegex(importSource);
206
+ const forImported = (names, selector) => anyOf(
207
+ ...names.map(
208
+ (name) => `Program:has(ImportDeclaration[source.value=${source}] > :matches(ImportSpecifier, ImportDefaultSpecifier)[local.name='${name}']) ${selector(name)}`
209
+ )
210
+ );
211
+ const sxOf = (name) => `JSXOpeningElement[name.name='${name}'] > JSXAttribute[name.name='sx']`;
212
+ return {
213
+ "spacing-step": {
214
+ selector: anyOf(
215
+ ...under(`${SX} Property[key.name=${SPACING_KEY}]`, [
216
+ ...signed(OFF_SCALE),
217
+ `ArrayExpression > ${OFF_SCALE}`,
218
+ `ObjectExpression > Property > ${OFF_SCALE}`
219
+ ]),
220
+ ...under(
221
+ `Property[key.name=${SPACING_KEY}]`,
222
+ signed(OFF_SCALE).map((n) => `${SPACING_CALL} > ${n}`)
223
+ )
224
+ ),
225
+ message: `Spacing off the Meridian scale: margin, padding and gap use ${spacingSteps.SPACING_STEPS.join(", ")}.`
226
+ },
227
+ "px-spacing": {
228
+ selector: anyOf(
229
+ ...under(`Property[key.name=${SPACING_KEY}]`, [
230
+ PX_STRING,
231
+ `ArrayExpression > ${PX_STRING}`,
232
+ `ObjectExpression > Property > ${PX_STRING}`
233
+ ]),
234
+ ...under(
235
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_KEY}]`,
236
+ signed(NON_ZERO)
237
+ )
238
+ ),
239
+ message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` are px."
240
+ },
241
+ "filled-icon": {
242
+ selector: anyOf(
243
+ "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
244
+ "ImportDeclaration[source.value='@mui/icons-material'][importKind!='type'] > ImportSpecifier[importKind!='type'][imported.name!=/(Outlined|Rounded|Sharp|TwoTone)$/]"
245
+ ),
246
+ message: "Filled MUI icon: import the Outlined variant."
247
+ },
248
+ "alpha-white-black": {
249
+ selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
250
+ message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
251
+ },
252
+ "hex-color": {
253
+ selector: STYLE_STRING(
254
+ "/(^|[\\s(,])#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b/"
255
+ ),
256
+ message: "Hardcoded hex color: use a palette token."
257
+ },
258
+ "rgb-color": {
259
+ selector: STYLE_STRING("/(^|[^a-z])rgba?\\(/"),
260
+ message: "Hardcoded rgb()/rgba() color: use a palette token."
261
+ },
262
+ "typography-style": {
263
+ selector: anyOf(
264
+ ...under(
265
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name=/^(fontWeight|fontFamily)$/]`,
266
+ [`Literal[value!=/^(${CSS_WIDE})$/]`]
267
+ ),
268
+ ...under(
269
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name='lineHeight']`,
270
+ [`Literal[value!=/^(${CSS_WIDE}|0|1|normal)$/]`]
271
+ ),
272
+ ...under(
273
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name='fontSize']:not(JSXOpeningElement[name.name=${ICON}] > JSXAttribute[name.name='sx'] Property)`,
274
+ [`Literal[value!=/^(${CSS_WIDE})$/]`]
275
+ )
276
+ ),
277
+ message: "Hardcoded typography: use `<Typography variant>` (and `weight`), `sx={{ typography: 'body2' }}` or `...theme.typography.body2`."
278
+ },
279
+ "external-link": {
280
+ selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
281
+ message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
282
+ },
283
+ "data-name": {
284
+ selector: forImported(
285
+ MERIDIAN_COMPONENTS,
286
+ (name) => `JSXOpeningElement[name.name='${name}'] > JSXAttribute[name.name='data-name']`
287
+ ),
288
+ message: "`data-name` on a Meridian component overrides its own marker: set it on a wrapper, or drop it."
289
+ },
290
+ "literal-a11y-text": {
291
+ selector: anyOf(
292
+ ...["", "JSXExpressionContainer > "].map(
293
+ (wrapper) => `JSXAttribute[name.name=/^(aria-label|placeholder)$/] > ${wrapper}Literal[value=/[A-Za-z]/]`
294
+ )
295
+ ),
296
+ message: "Hardcoded `aria-label` / `placeholder`: translate it with the i18n layer, and keep test ids out of it."
297
+ },
298
+ "raw-shadow-radius": {
299
+ selector: anyOf(
300
+ "Property[key.name='boxShadow'] > Literal[value=/\\d/]",
301
+ "Property[key.name='borderRadius'] > Literal[value=/^\\d+(\\.\\d+)?px$/]"
302
+ ),
303
+ message: "Hardcoded shadow or radius: use `theme.shadows[n]` / `theme.shape.borderRadius`."
304
+ },
305
+ "styled-meridian": {
306
+ selector: forImported(
307
+ MERIDIAN_COMPONENTS,
308
+ (name) => `CallExpression[callee.name='styled'] > Identifier[name='${name}']`
309
+ ),
310
+ message: "`styled()` of a Meridian component restyles it locally: use its props/variants, or raise the gap upstream in Meridian."
311
+ },
312
+ "visual-sx": {
313
+ selector: forImported(
314
+ MERIDIAN_COMPONENTS,
315
+ (name) => `${sxOf(name)} :matches(Property[key.name=${VISUAL_KEY}], Property[key.name=${COLOR_KEY}] Literal[value!=${PALETTE_VALUE}])`
316
+ ),
317
+ message: "Visual `sx` on a Meridian component overrides its design: keep `sx` to layout (spacing, size, position) and use its props/variants for the look."
318
+ },
319
+ "disabled-without-reason": {
320
+ selector: forImported(
321
+ ["Button", "IconButton", "SplitButton"],
322
+ (name) => `JSXOpeningElement[name.name='${name}']:has(> JSXAttribute[name.name='disabled']:not([value.expression.value=false])):not(:has(> JSXAttribute[name.name=/^(tooltip|aria-describedby)$/]))`
323
+ ),
324
+ message: "Disabled without a reason: pass `tooltip` (or `aria-describedby`) saying why it can’t be used."
325
+ },
326
+ "disabled-cursor": {
327
+ selector: "Property[key.name='cursor'] > Literal[value='not-allowed']",
328
+ message: "Don't tweak the cursor for disabled states: the component already styles them."
329
+ }
330
+ };
331
+ }
332
+ const meridianDesignRules = createMeridianDesignRules();
299
333
  const PROPS_TYPES = new Set(MERIDIAN_PROPS_TYPES);
300
334
  const replacedBy = (names) => names.flatMap((n) => PROPS_TYPES.has(n) ? [n, `${n}Props`] : [n]);
301
335
  const USE_MERIDIAN = "Import from @carto/meridian-ds/components instead.";
@@ -329,5 +363,6 @@ const meridianRestrictedImports = {
329
363
  message: USE_MERIDIAN
330
364
  }))
331
365
  };
366
+ exports.createMeridianDesignRules = createMeridianDesignRules;
332
367
  exports.meridianDesignRules = meridianDesignRules;
333
368
  exports.meridianRestrictedImports = meridianRestrictedImports;
@@ -176,7 +176,12 @@ const NON_ZERO = "Literal[raw=/^[0-9.]+$/][raw!=/^0+(\\.0+)?$/]";
176
176
  const exactly = (...alternatives) => `/^(${alternatives.join("|")})$/`;
177
177
  const SPACING = "[mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap";
178
178
  const SPACING_KEY = exactly(SPACING);
179
- const MERIDIAN_COMPONENT = exactly(...MERIDIAN_COMPONENTS);
179
+ const MERIDIAN_IMPORT = /^@carto\/meridian-ds(\/|$)/;
180
+ const CSS_WIDE = "inherit|initial|unset|revert|revert-layer";
181
+ const PALETTE_VALUE = `/^(${CSS_WIDE}|currentColor|transparent|divider|[a-z]+\\.[A-Za-z0-9]+(\\.[A-Za-z0-9]+)?)$/`;
182
+ const ICON = "/(Icon|Outlined|Rounded|Sharp|TwoTone)$/";
183
+ const VISUAL_KEY = "/^(background|border|border(Top|Right|Bottom|Left)?(Color|Width|Style)?|borderRadius|boxShadow|opacity|outline|textTransform|textDecoration|fill|stroke)$/";
184
+ const COLOR_KEY = "/^(color|bgcolor|backgroundColor)$/";
180
185
  const SX = ":matches(JSXAttribute[name.name='sx'], Property[key.name='sx'])";
181
186
  const STYLED_OR_STYLE = ":matches(CallExpression[callee.callee.name='styled'], CallExpression[callee.object.name='styled'], CallExpression[callee.callee.object.name='styled'], JSXAttribute[name.name='style'])";
182
187
  const SPACING_CALL = "CallExpression[callee.property.name='spacing']";
@@ -191,109 +196,138 @@ const WRAPPERS = [
191
196
  "TemplateLiteral > ConditionalExpression > "
192
197
  ];
193
198
  const under = (parent, nodes) => nodes.flatMap((node) => WRAPPERS.map((w) => `${parent} > ${w}${node}`));
194
- const meridianDesignRules = {
195
- "spacing-step": {
196
- selector: anyOf(
197
- ...under(`${SX} Property[key.name=${SPACING_KEY}]`, [
198
- ...signed(OFF_SCALE),
199
- `ArrayExpression > ${OFF_SCALE}`,
200
- `ObjectExpression > Property > ${OFF_SCALE}`
201
- ]),
202
- ...under(
203
- `Property[key.name=${SPACING_KEY}]`,
204
- signed(OFF_SCALE).map((n) => `${SPACING_CALL} > ${n}`)
205
- )
206
- ),
207
- message: `Spacing off the Meridian scale: margin, padding and gap use ${SPACING_STEPS.join(", ")}.`
208
- },
209
- "px-spacing": {
210
- selector: anyOf(
211
- ...under(`Property[key.name=${SPACING_KEY}]`, [
212
- PX_STRING,
213
- `ArrayExpression > ${PX_STRING}`,
214
- `ObjectExpression > Property > ${PX_STRING}`
215
- ]),
216
- ...under(
217
- `${STYLED_OR_STYLE} Property[key.name=${SPACING_KEY}]`,
218
- signed(NON_ZERO)
219
- )
220
- ),
221
- message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` are px."
222
- },
223
- "filled-icon": {
224
- selector: anyOf(
225
- "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
226
- "ImportDeclaration[source.value='@mui/icons-material'][importKind!='type'] > ImportSpecifier[importKind!='type'][imported.name!=/(Outlined|Rounded|Sharp|TwoTone)$/]"
227
- ),
228
- message: "Filled MUI icon: import the Outlined variant."
229
- },
230
- "alpha-white-black": {
231
- selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
232
- message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
233
- },
234
- "hex-color": {
235
- selector: STYLE_STRING(
236
- "/(^|[\\s(,])#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b/"
237
- ),
238
- message: "Hardcoded hex color: use a palette token."
239
- },
240
- "rgb-color": {
241
- selector: STYLE_STRING("/(^|[^a-z])rgba?\\(/"),
242
- message: "Hardcoded rgb()/rgba() color: use a palette token."
243
- },
244
- "typography-style": {
245
- selector: anyOf(
246
- ...under(
247
- `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name=/^(fontSize|fontWeight|lineHeight|fontFamily)$/]`,
248
- ["Literal[value!=/^(inherit|initial|unset|revert|revert-layer)$/]"]
249
- )
250
- ),
251
- message: "Hardcoded typography: use `<Typography variant>` (and `weight`), `sx={{ typography: 'body2' }}` or `...theme.typography.body2`."
252
- },
253
- "external-link": {
254
- selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
255
- message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
256
- },
257
- "system-props": {
258
- selector: "JSXOpeningElement[name.name=/^(Box|Stack|Grid|Grid2|Container)$/] > JSXAttribute[name.name=/^(m|p)[trblxy]?$/]",
259
- message: "MUI system props are deprecated: move spacing into `sx`, e.g. `<Box sx={{ px: 2 }}>`."
260
- },
261
- "data-name": {
262
- selector: `JSXOpeningElement[name.name=${MERIDIAN_COMPONENT}] > JSXAttribute[name.name='data-name']`,
263
- message: "`data-name` on a Meridian component overrides its own marker: set it on a wrapper, or drop it."
264
- },
265
- "literal-a11y-text": {
266
- selector: anyOf(
267
- ...["", "JSXExpressionContainer > "].map(
268
- (wrapper) => `JSXAttribute[name.name=/^(aria-label|placeholder)$/] > ${wrapper}Literal[value=/[A-Za-z]/]`
269
- )
270
- ),
271
- message: "Hardcoded `aria-label` / `placeholder`: translate it with the i18n layer, and keep test ids out of it."
272
- },
273
- "raw-shadow-radius": {
274
- selector: anyOf(
275
- "Property[key.name='boxShadow'] > Literal[value=/\\d/]",
276
- "Property[key.name='borderRadius'] > Literal[value=/^\\d+(\\.\\d+)?px$/]"
277
- ),
278
- message: "Hardcoded shadow or radius: use `theme.shadows[n]` / `theme.shape.borderRadius`."
279
- },
280
- "styled-meridian": {
281
- selector: `CallExpression[callee.name='styled'] > Identifier[name=${MERIDIAN_COMPONENT}]`,
282
- message: "`styled()` of a Meridian component restyles it locally: use its props/variants, or raise the gap upstream in Meridian."
283
- },
284
- "visual-sx": {
285
- selector: `JSXOpeningElement[name.name=${MERIDIAN_COMPONENT}] > JSXAttribute[name.name='sx'] Property[key.name=/^(color|bgcolor|background|backgroundColor|border|border(Top|Right|Bottom|Left)?(Color|Width|Style)?|borderRadius|boxShadow|opacity|outline|textTransform|textDecoration|fill|stroke)$/]`,
286
- message: "Visual `sx` on a Meridian component overrides its design: keep `sx` to layout (spacing, size, position) and use its props/variants for the look."
287
- },
288
- "disabled-without-reason": {
289
- selector: "JSXOpeningElement[name.name=/^(Button|IconButton|SplitButton)$/]:has(> JSXAttribute[name.name='disabled']:not([value.expression.value=false])):not(:has(> JSXAttribute[name.name=/^(tooltip|aria-describedby)$/]))",
290
- message: "Disabled without a reason: pass `tooltip` (or `aria-describedby`) saying why it can’t be used."
291
- },
292
- "disabled-cursor": {
293
- selector: "Property[key.name='cursor'] > Literal[value='not-allowed']",
294
- message: "Don't tweak the cursor for disabled states: the component already styles them."
295
- }
296
- };
199
+ const esqueryRegex = (re) => `/${re.source.replace(/\\?\//g, "\\x2F")}/`;
200
+ function createMeridianDesignRules({
201
+ importSource = MERIDIAN_IMPORT
202
+ } = {}) {
203
+ const source = esqueryRegex(importSource);
204
+ const forImported = (names, selector) => anyOf(
205
+ ...names.map(
206
+ (name) => `Program:has(ImportDeclaration[source.value=${source}] > :matches(ImportSpecifier, ImportDefaultSpecifier)[local.name='${name}']) ${selector(name)}`
207
+ )
208
+ );
209
+ const sxOf = (name) => `JSXOpeningElement[name.name='${name}'] > JSXAttribute[name.name='sx']`;
210
+ return {
211
+ "spacing-step": {
212
+ selector: anyOf(
213
+ ...under(`${SX} Property[key.name=${SPACING_KEY}]`, [
214
+ ...signed(OFF_SCALE),
215
+ `ArrayExpression > ${OFF_SCALE}`,
216
+ `ObjectExpression > Property > ${OFF_SCALE}`
217
+ ]),
218
+ ...under(
219
+ `Property[key.name=${SPACING_KEY}]`,
220
+ signed(OFF_SCALE).map((n) => `${SPACING_CALL} > ${n}`)
221
+ )
222
+ ),
223
+ message: `Spacing off the Meridian scale: margin, padding and gap use ${SPACING_STEPS.join(", ")}.`
224
+ },
225
+ "px-spacing": {
226
+ selector: anyOf(
227
+ ...under(`Property[key.name=${SPACING_KEY}]`, [
228
+ PX_STRING,
229
+ `ArrayExpression > ${PX_STRING}`,
230
+ `ObjectExpression > Property > ${PX_STRING}`
231
+ ]),
232
+ ...under(
233
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_KEY}]`,
234
+ signed(NON_ZERO)
235
+ )
236
+ ),
237
+ message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` are px."
238
+ },
239
+ "filled-icon": {
240
+ selector: anyOf(
241
+ "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
242
+ "ImportDeclaration[source.value='@mui/icons-material'][importKind!='type'] > ImportSpecifier[importKind!='type'][imported.name!=/(Outlined|Rounded|Sharp|TwoTone)$/]"
243
+ ),
244
+ message: "Filled MUI icon: import the Outlined variant."
245
+ },
246
+ "alpha-white-black": {
247
+ selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
248
+ message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
249
+ },
250
+ "hex-color": {
251
+ selector: STYLE_STRING(
252
+ "/(^|[\\s(,])#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\\b/"
253
+ ),
254
+ message: "Hardcoded hex color: use a palette token."
255
+ },
256
+ "rgb-color": {
257
+ selector: STYLE_STRING("/(^|[^a-z])rgba?\\(/"),
258
+ message: "Hardcoded rgb()/rgba() color: use a palette token."
259
+ },
260
+ "typography-style": {
261
+ selector: anyOf(
262
+ ...under(
263
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name=/^(fontWeight|fontFamily)$/]`,
264
+ [`Literal[value!=/^(${CSS_WIDE})$/]`]
265
+ ),
266
+ ...under(
267
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name='lineHeight']`,
268
+ [`Literal[value!=/^(${CSS_WIDE}|0|1|normal)$/]`]
269
+ ),
270
+ ...under(
271
+ `:matches(${SX}, ${STYLED_OR_STYLE}) Property[key.name='fontSize']:not(JSXOpeningElement[name.name=${ICON}] > JSXAttribute[name.name='sx'] Property)`,
272
+ [`Literal[value!=/^(${CSS_WIDE})$/]`]
273
+ )
274
+ ),
275
+ message: "Hardcoded typography: use `<Typography variant>` (and `weight`), `sx={{ typography: 'body2' }}` or `...theme.typography.body2`."
276
+ },
277
+ "external-link": {
278
+ selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
279
+ message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
280
+ },
281
+ "data-name": {
282
+ selector: forImported(
283
+ MERIDIAN_COMPONENTS,
284
+ (name) => `JSXOpeningElement[name.name='${name}'] > JSXAttribute[name.name='data-name']`
285
+ ),
286
+ message: "`data-name` on a Meridian component overrides its own marker: set it on a wrapper, or drop it."
287
+ },
288
+ "literal-a11y-text": {
289
+ selector: anyOf(
290
+ ...["", "JSXExpressionContainer > "].map(
291
+ (wrapper) => `JSXAttribute[name.name=/^(aria-label|placeholder)$/] > ${wrapper}Literal[value=/[A-Za-z]/]`
292
+ )
293
+ ),
294
+ message: "Hardcoded `aria-label` / `placeholder`: translate it with the i18n layer, and keep test ids out of it."
295
+ },
296
+ "raw-shadow-radius": {
297
+ selector: anyOf(
298
+ "Property[key.name='boxShadow'] > Literal[value=/\\d/]",
299
+ "Property[key.name='borderRadius'] > Literal[value=/^\\d+(\\.\\d+)?px$/]"
300
+ ),
301
+ message: "Hardcoded shadow or radius: use `theme.shadows[n]` / `theme.shape.borderRadius`."
302
+ },
303
+ "styled-meridian": {
304
+ selector: forImported(
305
+ MERIDIAN_COMPONENTS,
306
+ (name) => `CallExpression[callee.name='styled'] > Identifier[name='${name}']`
307
+ ),
308
+ message: "`styled()` of a Meridian component restyles it locally: use its props/variants, or raise the gap upstream in Meridian."
309
+ },
310
+ "visual-sx": {
311
+ selector: forImported(
312
+ MERIDIAN_COMPONENTS,
313
+ (name) => `${sxOf(name)} :matches(Property[key.name=${VISUAL_KEY}], Property[key.name=${COLOR_KEY}] Literal[value!=${PALETTE_VALUE}])`
314
+ ),
315
+ message: "Visual `sx` on a Meridian component overrides its design: keep `sx` to layout (spacing, size, position) and use its props/variants for the look."
316
+ },
317
+ "disabled-without-reason": {
318
+ selector: forImported(
319
+ ["Button", "IconButton", "SplitButton"],
320
+ (name) => `JSXOpeningElement[name.name='${name}']:has(> JSXAttribute[name.name='disabled']:not([value.expression.value=false])):not(:has(> JSXAttribute[name.name=/^(tooltip|aria-describedby)$/]))`
321
+ ),
322
+ message: "Disabled without a reason: pass `tooltip` (or `aria-describedby`) saying why it can’t be used."
323
+ },
324
+ "disabled-cursor": {
325
+ selector: "Property[key.name='cursor'] > Literal[value='not-allowed']",
326
+ message: "Don't tweak the cursor for disabled states: the component already styles them."
327
+ }
328
+ };
329
+ }
330
+ const meridianDesignRules = createMeridianDesignRules();
297
331
  const PROPS_TYPES = new Set(MERIDIAN_PROPS_TYPES);
298
332
  const replacedBy = (names) => names.flatMap((n) => PROPS_TYPES.has(n) ? [n, `${n}Props`] : [n]);
299
333
  const USE_MERIDIAN = "Import from @carto/meridian-ds/components instead.";
@@ -328,6 +362,7 @@ const meridianRestrictedImports = {
328
362
  }))
329
363
  };
330
364
  export {
365
+ createMeridianDesignRules,
331
366
  meridianDesignRules,
332
367
  meridianRestrictedImports
333
368
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.2.1-alpha.de0b18a.359",
2
+ "version": "5.2.1",
3
3
  "conventions": "# Meridian usage conventions (for code-generating agents)\n\nSystem-level rules an agent should apply across all components.\n\nMeridian is the source of truth: when an existing codebase pattern contradicts Meridian, follow Meridian — don't replicate the contradicting local pattern.\n\n## Choosing a component\n\n- Don't hand-roll what the design system already provides. Before writing a\n custom component, a styled wrapper, or a block of primitives (a modal,\n chip/tag, card, empty state, dropdown, tooltip, copy-to-clipboard input,\n etc.), confirm no Meridian component covers it — reuse or extend the existing one instead of rebuilding it.\n- Prefer a Meridian component over a bare MUI one whenever a Meridian wrapper\n exists — the wrappers carry CARTO theming, a11y defaults, and narrowed APIs. MUI is correct for layout primitives (`Box`/`Grid`/`Stack`/`Container`) and for gaps Meridian doesn't fill (e.g. date pickers); \"prefer Meridian\" is not \"never MUI\".\n- Resolve \"which component\" by reading each one's `description` + `keywords`,\n then confirm with its `decisionTree` (the conditions that point elsewhere) and `limitations` (what it lacks — check before you downgrade to it).\n Don't infer selection from prop lists.\n- Common hand-rolled patterns and their Meridian replacement:\n - `<a target=\"_blank\">` → `Link external`\n - modal built from `Box` → `Dialog` + `DialogHeader`/`DialogContent`/`DialogFooter`\n - pill/badge `Box` → `Tag`\n - custom select/dropdown → `SelectField`, or `FilterDropdown` for filters\n - label + required/optional mark → `LabelWithIndicator`\n - read-only input with a copy button → `CopiableInputText`\n - custom input → `TextField` (themed by Meridian; pick its `variant`) or `SearchField`\n - manual ellipsis + tooltip → `EllipsisWithTooltip`\n - status/feedback `Box` → `Alert`\n - form label/help text → `InputLabel`/`FormHelperText` (themed by Meridian)\n- Compose before customizing: a cluster of `styled()` wrappers that mirrors a composite is a smell (e.g. a hand-built row instead of `ListItem` + `ListItemIcon iconProps` + `ListItemText` + `ListItemRightContent`). Don't trust a code comment claiming the design system can't do something — check the component's props first.\n\n## Icons\n\n- Source icons from Meridian's custom-icons first, then `@mui/icons-material`;\n add a local SVG only when neither has an equivalent. Don't duplicate an icon\n the design system already ships.\n- Find the right custom icon with `meridian_list_icons` — match by name or keyword (e.g. \"warehouse\" → `Bigquery`), import it from `@carto/meridian-ds/custom-icons`, and render `<Name />`. Every icon shares one prop type, `CustomIconProps` (MUI `SvgIconProps` — `color`/`fontSize`/`sx` — plus `width`/`height`).\n- To confirm a shortlisted icon by sight (or match an icon in a design you're implementing), call `meridian_get_screenshot` with `name: 'icons'` and Read the returned image path — a labeled grid of every icon.\n- Use the standard icon sizes — small 12px, medium (default) 18px, large 24px;\n a value off this scale (e.g. 16, 20) needs design sign-off. Prefer the\n outlined variants.\n- `IconWrapper`'s `size` is the outer footprint (steps 12/18/24/32/40/48), not the glyph size — the glyph inside is sized for you.\n- A new SVG must inherit, not bake in: set `fill='currentColor'` so it takes\n the parent's color, and use the standard size — don't hardcode\n color/opacity/dimensions.\n- For an icon color outside the palette `color` set, pass a theme token via\n `htmlColor` (e.g. `htmlColor={theme.palette.text.secondary}`) — never a\n hardcoded hex.\n\n## Accessibility\n\n- Any icon-only control needs an accessible name: `tooltip` or `aria-label`.\n- Don't disable a control without telling the user why — pass the reason as `tooltip` on `Button`/`IconButton`/`SplitButton` (it shows while disabled), or link it with `aria-describedby`.\n- An `aria-label` must be human-readable text describing the control, not a test id in disguise — use a separate test hook for that.\n- User-facing strings (labels, `aria-label`, placeholders, messages) go through the app's i18n layer, not hardcoded literals.\n- Interactive elements are real controls (`Button`, `IconButton`, `Link`, …) — never a `div`/`Box` with `onClick`.\n- Every input has a visible or accessible label, not just a placeholder; every image has `alt`.\n\n## Props\n\n- Two sources, both pinned to the installed `@carto/meridian-ds`: **exact props / types / JSDoc** come from the package's **type declarations** — read the `<Name>.d.ts` directly — while the **`meridian-design-system` MCP** (`meridian_*` tools) covers what types can't: which component, when, gotchas, and real examples.\n- A component's curation lists the high-value inherited MUI props worth knowing (`curation.mui`, e.g. `color`/`variant`/`size`); the rest of MUI's surface is available but rarely needed.\n- Don't use `sx`/inline styles to override a Meridian component's visual design (colors, typography, borders, sizing) — use its documented props and the theme tokens. (`sx` for layout/spacing on primitives is fine — see Styling.)\n- Prefer the prop over an override (`Typography noWrap`/`weight`, `fullWidth`, `Alert isSticky`, …). Don't restyle one component to imitate another or to fake a state — pick the component/variant that already looks that way (e.g. `TextField variant=\"standard\"`, or plain `Typography` for a read-only value). Avoid brittle descendant selectors into a component's internals.\n- Don't restate or cancel built-in styling: no wrapper re-applying a component's own border/radius/background, no `disablePadding` on a parent only to re-add padding on its children.\n- `!important` on a Meridian or themed component is an override smell — a design-system gap to raise upstream.\n- If no prop covers the visual change you need, treat it as a design-system gap: request the variant/behavior upstream in Meridian rather than permanently overriding the component locally — a local `styled()` of a Meridian component is a smell, not a fix.\n\n## Styling\n\n- Use theme tokens, never hardcoded values: colors from the palette (`text.*`, `background.*`, `divider`, semantic `*.main`) and spacing from the theme scale (`theme.spacing(n)`, or `sx` numeric spacing like `px: 2` / `mt: 1.5`) — no raw hex/rgb or px. Margin, padding and gap use steps `0.25, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 12, 15` (2–120px); `width`/`height` and positional offsets may use any step (`spacing-step` in `@carto/meridian-ds/eslint-rules` lints it). Get the exact values — palette keys, spacing base, type ramp, `shape.borderRadius`, breakpoints, shadows — from `meridian_get_tokens`.\n- Token details:\n - `sx` spacing keys (`m*`/`p*`/`gap`) resolve to the scale, but `top`/`right`/`bottom`/`left` don't (`top: 2` is 2px) — use `theme.spacing()` there.\n - In `styled()`, always use `theme.spacing()`. `style={}` isn't theme-aware — use `sx`.\n - Borders use `divider`; radius comes from `shape.borderRadius`; elevation from `theme.shadows[n]`.\n - Translucent black/white use `palette.black[N]`/`palette.white[N]` (N = 90, 60, 40, 25, 12, 8, 4), not `alpha()`.\n - Motion uses `theme.transitions` and respects `prefers-reduced-motion`; responsive rules use `theme.breakpoints`.\n- Text uses `<Typography variant=…>` (with the `weight` prop for emphasis), never hardcoded `fontSize`/`fontWeight`/`lineHeight`/`fontFamily`. Text that can overflow gets `EllipsisWithTooltip` (or `noWrap` when the full text isn't needed).\n- Meridian's type scale is custom and does not match MUI defaults (e.g. `body2`/`caption` differ from MUI's sizes, and there are Meridian-only variants like `code1`/`code2`/`code3` and `overlineDelicate`). Pick the variant by role from Typography's variant list (`meridian_get_examples('Typography')`) — don't assume a px size or map px → variant yourself.\n- `sx` is the right tool for one-off layout/spacing on primitives (`Box`/`Grid`/`Stack`), not the deprecated MUI system props\n (`<Box px={2}>` → `<Box sx={{ px: 2 }}>`). This is layout, distinct from restyling a Meridian component (see Props).\n\n- A reusable component doesn't bake in its placement: no absolute positioning, outer margin, or fixed outer size — accept `sx` and let the caller place it.\n\n## Examples\n\n- Never invent example code when a story exists — use the component's real story source (`meridian_get_examples`). The lead Usage story is the copy-paste starting point; the later stories are illustrative (variants, edge cases) and may carry story-arg shells, so don't copy one wholesale as your baseline.\n",
4
4
  "components": [
5
5
  {
@@ -7,8 +7,12 @@ export interface RestrictedSyntaxEntry {
7
7
  * Meridian design rules as `no-restricted-syntax` entries, keyed by name — spread
8
8
  * them into the rule's options. Generated from the installed Meridian version
9
9
  * (e.g. the spacing scale), so they stay in sync with the pinned package.
10
+ * Component rules apply only where the component is imported from `importSource`,
11
+ * so a same-named one from elsewhere (`react-router-dom`'s `Link`) isn't flagged.
10
12
  */
11
- export declare const meridianDesignRules: {
13
+ export declare function createMeridianDesignRules({ importSource, }?: {
14
+ importSource?: RegExp;
15
+ }): {
12
16
  'spacing-step': {
13
17
  selector: string;
14
18
  message: string;
@@ -41,7 +45,66 @@ export declare const meridianDesignRules: {
41
45
  selector: string;
42
46
  message: string;
43
47
  };
44
- 'system-props': {
48
+ 'data-name': {
49
+ selector: string;
50
+ message: string;
51
+ };
52
+ 'literal-a11y-text': {
53
+ selector: string;
54
+ message: string;
55
+ };
56
+ 'raw-shadow-radius': {
57
+ selector: string;
58
+ message: string;
59
+ };
60
+ 'styled-meridian': {
61
+ selector: string;
62
+ message: string;
63
+ };
64
+ 'visual-sx': {
65
+ selector: string;
66
+ message: string;
67
+ };
68
+ 'disabled-without-reason': {
69
+ selector: string;
70
+ message: string;
71
+ };
72
+ 'disabled-cursor': {
73
+ selector: string;
74
+ message: string;
75
+ };
76
+ };
77
+ /** `createMeridianDesignRules()` for code importing from `@carto/meridian-ds`. */
78
+ export declare const meridianDesignRules: {
79
+ 'spacing-step': {
80
+ selector: string;
81
+ message: string;
82
+ };
83
+ 'px-spacing': {
84
+ selector: string;
85
+ message: string;
86
+ };
87
+ 'filled-icon': {
88
+ selector: string;
89
+ message: string;
90
+ };
91
+ 'alpha-white-black': {
92
+ selector: string;
93
+ message: string;
94
+ };
95
+ 'hex-color': {
96
+ selector: string;
97
+ message: string;
98
+ };
99
+ 'rgb-color': {
100
+ selector: string;
101
+ message: string;
102
+ };
103
+ 'typography-style': {
104
+ selector: string;
105
+ message: string;
106
+ };
107
+ 'external-link': {
45
108
  selector: string;
46
109
  message: string;
47
110
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/eslint-rules/index.ts"],"names":[],"mappings":"AAQA,wCAAwC;AACxC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAmDD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsHiB,CAAA;AAUjD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;CA8BrC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/eslint-rules/index.ts"],"names":[],"mappings":"AAQA,wCAAwC;AACxC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAgED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,YAA8B,GAC/B,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2JhC;AAED,kFAAkF;AAClF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA8B,CAAA;AAU9D;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;CA8BrC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carto/meridian-ds",
3
- "version": "5.2.1-alpha.de0b18a.359",
3
+ "version": "5.2.1",
4
4
  "description": "CARTO Meridian Design System",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -43,10 +43,6 @@
43
43
  "clean:all": "yarn run clean:node_modules && yarn run clean:build",
44
44
  "clean:node_modules": "rm -rf ./node_modules",
45
45
  "clean:build": "rm -rf ./coverage ./reports ./dist",
46
- "postversion": "yarn postversion:check && yarn postversion:commit && yarn postversion:push",
47
- "postversion:check": "yarn lint && yarn test",
48
- "postversion:commit": "node scripts/postversion-commit.js",
49
- "postversion:push": "git push && git push --tags",
50
46
  "prepare": "husky",
51
47
  "prepack": "yarn clean:build && yarn build && yarn metadata:build && yarn build:mcp",
52
48
  "figma:connect": "bash -c '[ -f .env ] && source .env; export FIGMA_ACCESS_TOKEN; figma connect --token=$FIGMA_ACCESS_TOKEN \"$@\"' --",