@carto/meridian-ds 5.1.2 → 5.1.4-alpha.0360fb1.341

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
@@ -2,8 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - feat(ai-metadata): compact MCP, design-review conventions and Meridian ESLint design rules [sc-578601] [sc-578639] [sc-579313] [#563](https://github.com/CartoDB/meridian-ds/pull/563)
6
+
5
7
  ## 5.0
6
8
 
9
+ ### 5.1.3
10
+
11
+ - fix(ai-metadata): allow fractional spacing steps; give design team ownership of design-system paths [sc-577069] [#558](https://github.com/CartoDB/meridian-ds/pull/558)
12
+ - feat(ci): notify Slack when a PR needs design review [#558](https://github.com/CartoDB/meridian-ds/pull/558)
13
+
7
14
  ### 5.1.2
8
15
 
9
16
  - fix(dialog-header): Name the dialog only by its title [sc-577444] [#556](https://github.com/CartoDB/meridian-ds/pull/556)
@@ -40,7 +40,7 @@ var VERSION = "0.0.0";
40
40
  var TOOLS = [
41
41
  {
42
42
  name: "meridian_list_components",
43
- description: "List all Meridian components with their description and keywords, plus a family signal where relevant (`subComponents` for a compound component, `variants` for sibling components chosen between \u2014 each with a one-line description). Start here: the catalog is small, so read it and pick the component whose description/keywords match your need. Rows flagged `documented: false` are not yet curated (no when-to-use/decisionTree/prop guidance): most still have example stories via get_examples; a few are known-only \u2014 real, importable components with no stories yet, for which get_component/get_examples just point at the type declarations.",
43
+ description: "The Meridian component catalog \u2014 start here. Each row has a description and keywords, plus by name: `subComponents` (parts it's composed with), `variants` (siblings chosen between) and `relatedExports` (hooks/utils/constants). Pick by meaning (there's no search tool), then call meridian_get_component. `documented: false` marks components with no curation yet \u2014 lean on meridian_get_examples and the .d.ts for those.",
44
44
  inputSchema: {
45
45
  type: "object",
46
46
  properties: {},
@@ -49,19 +49,23 @@ var TOOLS = [
49
49
  },
50
50
  {
51
51
  name: "meridian_get_component",
52
- description: "Curation for one component: when to use it, its decisionTree (when to reach for a different component), any limitations (capabilities it lacks vs. similar components \u2014 check before downgrading from one), and the high-value inherited MUI props (curation.mui, some with an aiHint). Own props, types, and JSDoc are NOT here \u2014 read the shipped .d.ts (the propTypes field gives the exact command). For a not-yet-curated component (`documented: false`) it returns a note + .d.ts pointer in place of curation \u2014 plus its example stories when any exist (a known-only component has none, so rely on the .d.ts). Sub-components and variants (`DialogHeader`, `MultipleAutocomplete`, \u2026) have no standalone catalog entry \u2014 `meridian_get_component`/`meridian_get_examples` on them return a pointer to the parent (check its `curation.subComponents`/`curation.variants`).",
52
+ description: "Everything to use a component, in one call: when to use it, its decisionTree (when another component fits better), limitations, high-value inherited MUI props (`curation.mui`), family members with descriptions, the lead usage story's code (`examples.stories[0].code`) and the other story ids. Own props/types/JSDoc ship in the .d.ts \u2014 `propTypes` gives the command. Pass `names` for several components at once. A family member's name (e.g. `DialogHeader`) returns a pointer to its parent.",
53
53
  inputSchema: {
54
54
  type: "object",
55
55
  properties: {
56
- name: { type: "string", description: 'Component name, e.g. "Button"' }
56
+ name: { type: "string", description: 'Component name, e.g. "Button"' },
57
+ names: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: 'Several component names, e.g. ["SplitButton", "Menu"]. Returns one result per name, in order. Use instead of `name`.'
61
+ }
57
62
  },
58
- required: ["name"],
59
63
  additionalProperties: false
60
64
  }
61
65
  },
62
66
  {
63
67
  name: "meridian_get_examples",
64
- description: "Real, copy-able example code (per Storybook story, with imports), shipped in the package. With no storyId: the lead usage story and the notable stories in full, plus an 'Other Stories' list (name + id) of the rest. Pass a storyId (from that list or meridian_get_component) to get just that story's full code. Sub-components and variants (`DialogHeader`, `MultipleAutocomplete`, \u2026) have no standalone catalog entry \u2014 `meridian_get_component`/`meridian_get_examples` on them return a pointer to the parent (check its `curation.subComponents`/`curation.variants`).",
68
+ description: "Copy-able example code from a component's stories. With no storyId: the lead usage story and the notable stories in full, plus an index (name + id) of the rest. With a storyId: just that story. Family member names point to their parent, as in meridian_get_component.",
65
69
  inputSchema: {
66
70
  type: "object",
67
71
  properties: {
@@ -77,7 +81,7 @@ var TOOLS = [
77
81
  },
78
82
  {
79
83
  name: "meridian_get_conventions",
80
- description: "Mandatory system-wide Meridian rules to apply whenever generating or editing UI (component selection, a11y, i18n, styling/theme tokens, props, examples). Call this once per session BEFORE writing code and follow it \u2014 these rules are not repeated in the per-component data.",
84
+ description: "Mandatory system-wide Meridian rules (component choice, a11y, i18n, styling/tokens, props, examples). Call once per session BEFORE writing UI code and follow them \u2014 the per-component data doesn't repeat them.",
81
85
  inputSchema: {
82
86
  type: "object",
83
87
  properties: {},
@@ -86,7 +90,7 @@ var TOOLS = [
86
90
  },
87
91
  {
88
92
  name: "meridian_list_icons",
89
- description: "List every Meridian custom icon by name, with search keywords where curated (e.g. Bigquery \u2192 warehouse/database). There's no search tool \u2014 match by meaning against the name + keywords. Import the one you pick from '@carto/meridian-ds/custom-icons' and render it as `<Name />`. All icons share one prop type (CustomIconProps): MUI SvgIconProps \u2014 `color`, `fontSize`, `sx`, \u2026 \u2014 plus `width`/`height`; the result's `props` field has the details and the .d.ts pointer. Prefer a Meridian custom icon over an ad-hoc SVG or a MUI icon.",
93
+ description: "Every Meridian custom icon by name, with search keywords where curated (e.g. `Bigquery: warehouse, database`). Match by meaning; import from '@carto/meridian-ds/custom-icons' and render `<Name />`. Prefer these over an ad-hoc SVG or a MUI icon.",
90
94
  inputSchema: {
91
95
  type: "object",
92
96
  properties: {},
@@ -95,7 +99,7 @@ var TOOLS = [
95
99
  },
96
100
  {
97
101
  name: "meridian_get_tokens",
98
- description: "The design tokens (exact values) to reference instead of hardcoding: palette (semantic text/background/action/divider, the intents, and the color scales), the typography ramp (per-variant fontSize/fontWeight/lineHeight/letterSpacing), spacing base, shape.borderRadius, breakpoints, and shadows. Use these with the rules in meridian_get_conventions (which says to use tokens, not raw hex/px) \u2014 this tool supplies the actual values. Projected from the theme, so it always matches the installed Meridian version. These are theme-level tokens (the global palette/type/spacing scale); for a component's internal spacing (e.g. Button padding, List row gap) use meridian_get_component, not this.",
102
+ description: "Exact theme token values to use instead of hardcoding: palette, typography ramp, spacing base, shape.borderRadius, breakpoints and shadows \u2014 projected from the installed version's theme. Theme-level only: a component's internal spacing comes from meridian_get_component.",
99
103
  inputSchema: {
100
104
  type: "object",
101
105
  properties: {},
@@ -104,7 +108,7 @@ var TOOLS = [
104
108
  },
105
109
  {
106
110
  name: "meridian_get_screenshot",
107
- description: "Returns the filesystem path to a labeled reference screenshot. name='icons' is a grid of EVERY custom icon (each glyph captioned with its import name). Read that image to visually confirm an icon you shortlisted from meridian_list_icons, or to match one in a design you're implementing, then use its exact name. Deliberately returns a path (not an inline image) \u2014 portable, cheap per call, and reusable by a consumer app's picker. Requires a runtime that can read images (e.g. Claude Code); headless/non-multimodal callers can skip it. Call with no name to list the available screenshots.",
111
+ description: "Path to a labeled reference image. name='icons' is a grid of every custom icon captioned with its import name \u2014 Read it to confirm an icon visually. Needs an image-capable runtime. Omit name to list the available screenshots.",
108
112
  inputSchema: {
109
113
  type: "object",
110
114
  properties: {
@@ -168,7 +172,8 @@ function exampleStories(examples) {
168
172
  id: e.id,
169
173
  name: titleize(e.name),
170
174
  ...e.id === leadId ? { lead: true } : {},
171
- ...e.summary ? { summary: e.summary } : {}
175
+ ...e.summary ? { summary: e.summary } : {},
176
+ ...e.id === leadId ? { code: e.code } : {}
172
177
  }));
173
178
  }
174
179
  function listComponents() {
@@ -180,20 +185,14 @@ function listComponents() {
180
185
  // examples + the .d.ts are still available. Documented rows omit the flag.
181
186
  ...d.documented ? {} : { documented: false },
182
187
  keywords: d.curation.keywords ?? [],
183
- // Family signal: the parts a compound component is composed with, and the
184
- // sibling variants chosen between — each as { name, description } (same shape
185
- // as get_component) so an agent can pick a family member while scanning.
186
- ...d.curation.subComponents?.length ? { subComponents: d.curation.subComponents } : {},
187
- ...d.curation.variants?.length ? { variants: d.curation.variants } : {},
188
- // The public hooks/utils/constants that ship with this component — name,
189
- // kind, and description (same as subComponents/variants, so an agent can pick
190
- // one while scanning without a get_component round-trip). get_component adds
191
- // `import` (the entry point — constant across the family, so omitted here).
188
+ // Family members and related exports by name only; get_component carries
189
+ // their descriptions.
190
+ ...d.curation.subComponents?.length ? { subComponents: d.curation.subComponents.map(subName) } : {},
191
+ ...d.curation.variants?.length ? { variants: d.curation.variants.map(subName) } : {},
192
192
  ...d.curation.relatedExports?.length ? {
193
193
  relatedExports: d.curation.relatedExports.map((e) => ({
194
194
  name: relatedName(e),
195
- ...typeof e === "object" && e.kind ? { kind: e.kind } : {},
196
- ...typeof e === "object" && e.description ? { description: e.description } : {}
195
+ ...typeof e === "object" && e.kind ? { kind: e.kind } : {}
197
196
  }))
198
197
  } : {}
199
198
  }));
@@ -219,7 +218,7 @@ function getComponent(name) {
219
218
  // are already in `curation.mui` above.
220
219
  propTypes: `${compound}Own props, exact types, and JSDoc are not in this payload \u2014 they ship as TypeScript declarations. Read them from the installed package: \`${dtsGlob}\`. Import from '@carto/meridian-ds/components'.`,
221
220
  examples: doc.examples.length ? {
222
- note: "Stories are ordered by copy-value; the flagged lead is the starting point. Call meridian_get_examples(name) for the lead in full plus an index of the rest; pass a storyId for any specific one.",
221
+ note: "Stories are ordered by copy-value; the flagged lead (with its code) is the starting point. Call meridian_get_examples(name, storyId) for any other story.",
223
222
  stories: exampleStories(doc.examples)
224
223
  } : {
225
224
  note: "No example stories ship for this component yet \u2014 read its props from the .d.ts (see propTypes above).",
@@ -295,12 +294,15 @@ function getScreenshot(name) {
295
294
  return { path: shot.path, count: shot.count, hint: shot.hint };
296
295
  }
297
296
  function listIcons() {
298
- return {
299
- note: "Import from '@carto/meridian-ds/custom-icons' and render as <Name />. Match by name or keywords \u2014 there's no search tool. To see the glyphs, call meridian_get_screenshot({name:'icons'}) and Read the returned image path.",
300
- props: "Every icon is an identical forwardRef component sharing one prop type, CustomIconProps: MUI SvgIconProps \u2014 color, fontSize ('small'|'medium'|'large'|'inherit'), sx, htmlColor, titleAccess \u2014 plus width/height (number|string) to override the fontSize-based size. Prefer color/fontSize for theming/sizing; width/height only for a specific px size. The exact type ships at node_modules/@carto/meridian-ds/dist/types/custom-icons/types.d.ts (the per-icon <Name>.d.ts is identical boilerplate \u2014 no icon-specific props).",
301
- count: ICONS.length,
302
- icons: ICONS
303
- };
297
+ const lines = ICONS.map(
298
+ (i) => i.keywords?.length ? `${i.name}: ${i.keywords.join(", ")}` : i.name
299
+ );
300
+ return [
301
+ `${ICONS.length} Meridian custom icons. Import from '@carto/meridian-ds/custom-icons' and render as <Name />. Match by name or keywords \u2014 there's no search tool. To see the glyphs, call meridian_get_screenshot({name:'icons'}) and Read the returned image path.`,
302
+ "Props: every icon is an identical forwardRef component sharing one prop type, CustomIconProps: MUI SvgIconProps \u2014 color, fontSize ('small'|'medium'|'large'|'inherit'), sx, htmlColor, titleAccess \u2014 plus width/height (number|string) to override the fontSize-based size. Prefer color/fontSize for theming/sizing; width/height only for a specific px size. The exact type ships at node_modules/@carto/meridian-ds/dist/types/custom-icons/types.d.ts.",
303
+ "Icons (Name: keywords):",
304
+ ...lines
305
+ ].join("\n");
304
306
  }
305
307
  function callTool(name, args) {
306
308
  switch (name) {
@@ -313,7 +315,7 @@ function callTool(name, args) {
313
315
  case "meridian_get_screenshot":
314
316
  return getScreenshot(String(args.name ?? ""));
315
317
  case "meridian_get_component":
316
- return getComponent(String(args.name ?? ""));
318
+ return Array.isArray(args.names) ? args.names.map((n) => getComponent(String(n))) : getComponent(String(args.name ?? ""));
317
319
  case "meridian_get_examples":
318
320
  return getComponentExamples(
319
321
  String(args.name ?? ""),
@@ -325,22 +327,15 @@ function callTool(name, args) {
325
327
  throw new Error(`Unknown tool: ${name}`);
326
328
  }
327
329
  }
328
- var SERVER_INSTRUCTIONS = `Meridian Design System knowledge for writing React UI in an app that depends on @carto/meridian-ds.
330
+ var SERVER_INSTRUCTIONS = `Meridian Design System knowledge for writing React UI with @carto/meridian-ds.
329
331
 
330
- REQUIRED FIRST STEP: before writing or editing ANY Meridian UI, call meridian_get_conventions once and follow it for the rest of the session. These are mandatory cross-cutting rules (component selection, a11y, i18n, styling/theme tokens, props, examples) that the per-component data does NOT repeat \u2014 skipping them produces off-spec code.
332
+ REQUIRED FIRST STEP: call meridian_get_conventions once before writing or editing any Meridian UI, and follow it for the rest of the session \u2014 the per-component data doesn't repeat those rules.
331
333
 
332
- Flow:
333
- 1. meridian_get_conventions \u2014 read first; apply throughout the session.
334
- 2. meridian_list_components \u2014 read the (small) catalog and pick by description + keywords. There is no search tool; match by meaning (the model already knows "picker" \u2248 "dropdown" \u2248 "select").
335
- 3. meridian_get_component(name) \u2014 when to use it, its decisionTree (conditions that point to a different component), and the high-value inherited MUI props worth knowing (curation.mui; some carry an aiHint). It also returns the example story ids.
336
- 4. meridian_get_examples(name[, storyId]) \u2014 real, copy-able example code. With no storyId: the lead usage story and the notable stories in full plus a list of the rest; pass a storyId for one story's full code.
337
- 5. meridian_list_icons \u2014 when you need an icon, list the custom icons and match by name/keyword; import from '@carto/meridian-ds/custom-icons' and render as <Name />.
338
- 6. meridian_get_tokens \u2014 exact palette/typography/spacing/shape/breakpoint/shadow values to use instead of hardcoding (the conventions say use tokens; this gives the values).
339
- 7. meridian_get_screenshot \u2014 a labeled reference image (name='icons' \u2192 every icon); Read the returned path to visually confirm a shortlisted icon or match one from a design (image-capable runtimes only).
334
+ Then: meridian_list_components to pick \u2192 meridian_get_component (\`names: [...]\` for several; includes the lead example) \u2192 meridian_get_examples only for other stories. Icons: meridian_list_icons. Exact token values: meridian_get_tokens.
340
335
 
341
- Three tiers of components, by how much each carries (all are real and importable \u2014 always prefer one over a bare MUI equivalent): CURATED (most) have full when-to-use / decisionTree / promoted-prop guidance; LISTED (flagged "documented": false, with example stories) carry no curation yet \u2014 lean on get_examples + the .d.ts; KNOWN (flagged "documented": false, no stories) are real components not yet documented \u2014 get_component/get_examples point you at their type declarations (.d.ts).
336
+ Component tiers \u2014 all real and importable; prefer any of them over a bare MUI equivalent: CURATED (most) have full guidance; LISTED (\`documented: false\`, with stories) have no curation \u2014 use the examples + .d.ts; KNOWN (\`documented: false\`, no stories) have only the .d.ts.
342
337
 
343
- Exact own props, types, and JSDoc are NOT served here \u2014 they ship as TypeScript declarations in the installed package. Read them with: find node_modules/@carto/meridian-ds/dist/types -name '<Component>.d.ts'. Import the component from '@carto/meridian-ds/components'. Prefer a Meridian component over a bare MUI one whenever one exists.`;
338
+ Own props, types and JSDoc ship as TypeScript declarations: find node_modules/@carto/meridian-ds/dist/types -name '<Component>.d.ts'. Import components from '@carto/meridian-ds/components'.`;
344
339
  function send(msg) {
345
340
  process.stdout.write(JSON.stringify(msg) + "\n");
346
341
  }
@@ -365,7 +360,7 @@ function handle(req) {
365
360
  case "tools/call":
366
361
  try {
367
362
  const result = callTool(params?.name, params?.arguments ?? {});
368
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
363
+ const text = typeof result === "string" ? result : JSON.stringify(result);
369
364
  return reply({ content: [{ type: "text", text }] });
370
365
  } catch (err) {
371
366
  return reply({
@@ -15,6 +15,18 @@ Meridian is the source of truth: when an existing codebase pattern contradicts M
15
15
  - Resolve "which component" by reading each one's `description` + `keywords`,
16
16
  then confirm with its `decisionTree` (the conditions that point elsewhere) and `limitations` (what it lacks — check before you downgrade to it).
17
17
  Don't infer selection from prop lists.
18
+ - Common hand-rolled patterns and their Meridian replacement:
19
+ - `<a target="_blank">` → `Link external`
20
+ - modal built from `Box` → `Dialog` + `DialogHeader`/`DialogContent`/`DialogFooter`
21
+ - pill/badge `Box` → `Tag`
22
+ - custom select/dropdown → `SelectField`, or `FilterDropdown` for filters
23
+ - label + required/optional mark → `LabelWithIndicator`
24
+ - read-only input with a copy button → `CopiableInputText`
25
+ - custom input → `TextField` (themed by Meridian; pick its `variant`) or `SearchField`
26
+ - manual ellipsis + tooltip → `EllipsisWithTooltip`
27
+ - status/feedback `Box` → `Alert`
28
+ - form label/help text → `InputLabel`/`FormHelperText` (themed by Meridian)
29
+ - 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.
18
30
 
19
31
  ## Icons
20
32
 
@@ -26,6 +38,7 @@ Meridian is the source of truth: when an existing codebase pattern contradicts M
26
38
  - Use the standard icon sizes — small 12px, medium (default) 18px, large 24px;
27
39
  a value off this scale (e.g. 16, 20) needs design sign-off. Prefer the
28
40
  outlined variants.
41
+ - `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.
29
42
  - A new SVG must inherit, not bake in: set `fill='currentColor'` so it takes
30
43
  the parent's color, and use the standard size — don't hardcode
31
44
  color/opacity/dimensions.
@@ -39,22 +52,35 @@ Meridian is the source of truth: when an existing codebase pattern contradicts M
39
52
  - Don't disable a control without telling the user why — pair a disabled action with a tooltip explaining the requirement.
40
53
  - An `aria-label` must be human-readable text describing the control, not a test id in disguise — use a separate test hook for that.
41
54
  - User-facing strings (labels, `aria-label`, placeholders, messages) go through the app's i18n layer, not hardcoded literals.
55
+ - Interactive elements are real controls (`Button`, `IconButton`, `Link`, …) — never a `div`/`Box` with `onClick`.
56
+ - Every input has a visible or accessible label, not just a placeholder; every image has `alt`.
42
57
 
43
58
  ## Props
44
59
 
45
60
  - 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.
46
61
  - 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.
47
62
  - 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.)
63
+ - 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.
64
+ - 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.
65
+ - `!important` on a Meridian or themed component is an override smell — a design-system gap to raise upstream.
48
66
  - 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.
49
67
 
50
68
  ## Styling
51
69
 
52
- - Use theme tokens, never hardcoded values: colors from the palette (`text.*`, `background.*`, `divider`, semantic `*.main`) and spacing from the theme spacing scale — no raw hex/rgb or pixel literals for color or spacing. Use integer steps on the spacing scale (`theme.spacing(n)`, or `sx` numeric spacing like `px: 2` / `mt: 3` which resolve to it) — not fractional steps or raw px. Get the exact values — palette keys, spacing base, type ramp, `shape.borderRadius`, breakpoints, shadows — from `meridian_get_tokens`.
53
- - Text uses `<Typography variant=…>` (with the `weight` prop for emphasis), never hardcoded `fontSize`/`fontWeight`/`lineHeight`/`fontFamily`.
70
+ - 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 offsets 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` 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`.
71
+ - Token details:
72
+ - `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.
73
+ - In `styled()`, always use `theme.spacing()`. `style={}` isn't theme-aware — use `sx`.
74
+ - Borders use `divider`; radius comes from `shape.borderRadius`; elevation from `theme.shadows[n]`.
75
+ - Translucent black/white use `palette.black[N]`/`palette.white[N]` (N = 90, 60, 40, 25, 12, 8, 4), not `alpha()`.
76
+ - Motion uses `theme.transitions` and respects `prefers-reduced-motion`; responsive rules use `theme.breakpoints`.
77
+ - 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).
54
78
  - 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.
55
79
  - `sx` is the right tool for one-off layout/spacing on primitives (`Box`/`Grid`/`Stack`), not the deprecated MUI system props
56
80
  (`<Box px={2}>` → `<Box sx={{ px: 2 }}>`). This is layout, distinct from restyling a Meridian component (see Props).
57
81
 
82
+ - 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.
83
+
58
84
  ## Examples
59
85
 
60
86
  - 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.
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const spacingSteps = require("../spacing-steps-DOjPPOKL.cjs");
4
+ const escape = (step) => String(step).replace(".", "\\.");
5
+ const STEP = `(0|${spacingSteps.SPACING_STEPS.map(escape).join("|")})`;
6
+ const OFF_SCALE = `Literal[raw=/^[0-9.]+$/][raw!=/^${STEP}$/]`;
7
+ const NON_ZERO = "Literal[raw=/^[0-9.]+$/][raw!=/^0+(\\.0+)?$/]";
8
+ const SPACING_KEY = "/^([mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap)$/";
9
+ const OFFSET_KEY = "/^(top|right|bottom|left|inset)$/";
10
+ const SPACING_OR_OFFSET_KEY = "/^([mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap|top|right|bottom|left|inset)$/";
11
+ const SX = ":matches(JSXAttribute[name.name='sx'], Property[key.name='sx'])";
12
+ 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'])";
13
+ const SPACING_CALL = "CallExpression[callee.property.name='spacing']";
14
+ const anyOf = (...selectors) => selectors.join(", ");
15
+ const meridianDesignRules = {
16
+ "spacing-step": {
17
+ selector: anyOf(
18
+ `${SX} Property[key.name=${SPACING_KEY}] > ${OFF_SCALE}`,
19
+ `${SX} Property[key.name=${SPACING_KEY}] > UnaryExpression > ${OFF_SCALE}`,
20
+ `${SX} Property[key.name=${SPACING_KEY}] > ArrayExpression > ${OFF_SCALE}`,
21
+ `${SX} Property[key.name=${SPACING_KEY}] > ObjectExpression > Property > ${OFF_SCALE}`,
22
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${SPACING_CALL} > ${OFF_SCALE}`,
23
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${SPACING_CALL} > UnaryExpression > ${OFF_SCALE}`,
24
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > TemplateLiteral > ${SPACING_CALL} > ${OFF_SCALE}`
25
+ ),
26
+ message: `Spacing off the Meridian scale: margin, padding, gap and offsets use ${spacingSteps.SPACING_STEPS.join(", ")}.`
27
+ },
28
+ "px-spacing": {
29
+ selector: anyOf(
30
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > Literal[value=/^-?\\d+(\\.\\d+)?px/]`,
31
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${NON_ZERO}`,
32
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_OR_OFFSET_KEY}] > UnaryExpression > ${NON_ZERO}`,
33
+ `${SX} Property[key.name=${OFFSET_KEY}] > ${NON_ZERO}`,
34
+ `${SX} Property[key.name=${OFFSET_KEY}] > UnaryExpression > ${NON_ZERO}`
35
+ ),
36
+ message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` and `sx` offsets are px."
37
+ },
38
+ "filled-icon": {
39
+ selector: "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
40
+ message: "Filled MUI icon: import the Outlined variant."
41
+ },
42
+ "alpha-white-black": {
43
+ selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
44
+ message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
45
+ },
46
+ "hex-color": {
47
+ selector: "Literal[value=/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/]",
48
+ message: "Hardcoded hex color: use a palette token."
49
+ },
50
+ "rgb-color": {
51
+ selector: "Literal[value=/^rgba?\\(/]",
52
+ message: "Hardcoded rgb()/rgba() color: use a palette token."
53
+ },
54
+ "typography-style": {
55
+ selector: `:matches(JSXAttribute[name.name=/^(sx|style)$/], CallExpression[callee.callee.name='styled'], CallExpression[callee.callee.object.name='styled']) Property[key.name=/^(fontSize|fontWeight|lineHeight|fontFamily)$/]`,
56
+ message: "Hardcoded typography: use `<Typography variant>` (and `weight`), or the theme typography tokens."
57
+ },
58
+ "external-link": {
59
+ selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
60
+ message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
61
+ }
62
+ };
63
+ exports.meridianDesignRules = meridianDesignRules;
@@ -0,0 +1,63 @@
1
+ import { S as SPACING_STEPS } from "../spacing-steps-CN4lgz20.js";
2
+ const escape = (step) => String(step).replace(".", "\\.");
3
+ const STEP = `(0|${SPACING_STEPS.map(escape).join("|")})`;
4
+ const OFF_SCALE = `Literal[raw=/^[0-9.]+$/][raw!=/^${STEP}$/]`;
5
+ const NON_ZERO = "Literal[raw=/^[0-9.]+$/][raw!=/^0+(\\.0+)?$/]";
6
+ const SPACING_KEY = "/^([mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap)$/";
7
+ const OFFSET_KEY = "/^(top|right|bottom|left|inset)$/";
8
+ const SPACING_OR_OFFSET_KEY = "/^([mp][trblxy]?|(margin|padding)(Top|Right|Bottom|Left|X|Y|Inline|Block)?|gap|rowGap|columnGap|top|right|bottom|left|inset)$/";
9
+ const SX = ":matches(JSXAttribute[name.name='sx'], Property[key.name='sx'])";
10
+ 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'])";
11
+ const SPACING_CALL = "CallExpression[callee.property.name='spacing']";
12
+ const anyOf = (...selectors) => selectors.join(", ");
13
+ const meridianDesignRules = {
14
+ "spacing-step": {
15
+ selector: anyOf(
16
+ `${SX} Property[key.name=${SPACING_KEY}] > ${OFF_SCALE}`,
17
+ `${SX} Property[key.name=${SPACING_KEY}] > UnaryExpression > ${OFF_SCALE}`,
18
+ `${SX} Property[key.name=${SPACING_KEY}] > ArrayExpression > ${OFF_SCALE}`,
19
+ `${SX} Property[key.name=${SPACING_KEY}] > ObjectExpression > Property > ${OFF_SCALE}`,
20
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${SPACING_CALL} > ${OFF_SCALE}`,
21
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${SPACING_CALL} > UnaryExpression > ${OFF_SCALE}`,
22
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > TemplateLiteral > ${SPACING_CALL} > ${OFF_SCALE}`
23
+ ),
24
+ message: `Spacing off the Meridian scale: margin, padding, gap and offsets use ${SPACING_STEPS.join(", ")}.`
25
+ },
26
+ "px-spacing": {
27
+ selector: anyOf(
28
+ `Property[key.name=${SPACING_OR_OFFSET_KEY}] > Literal[value=/^-?\\d+(\\.\\d+)?px/]`,
29
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_OR_OFFSET_KEY}] > ${NON_ZERO}`,
30
+ `${STYLED_OR_STYLE} Property[key.name=${SPACING_OR_OFFSET_KEY}] > UnaryExpression > ${NON_ZERO}`,
31
+ `${SX} Property[key.name=${OFFSET_KEY}] > ${NON_ZERO}`,
32
+ `${SX} Property[key.name=${OFFSET_KEY}] > UnaryExpression > ${NON_ZERO}`
33
+ ),
34
+ message: "Raw px spacing: use the theme scale — `sx` spacing numbers (`px: 2`) or `theme.spacing(n)`. Numbers in `styled()`/`style` and `sx` offsets are px."
35
+ },
36
+ "filled-icon": {
37
+ selector: "ImportDeclaration[source.value=/^@mui.icons-material.(?!.*(Outlined|Rounded|Sharp|TwoTone)$)/]",
38
+ message: "Filled MUI icon: import the Outlined variant."
39
+ },
40
+ "alpha-white-black": {
41
+ selector: "CallExpression[callee.name='alpha'] > MemberExpression[property.name=/^(white|black)$/][object.property.name='common']",
42
+ message: "`alpha()` on white/black: use the palette opacity shades `white[N]` / `black[N]`."
43
+ },
44
+ "hex-color": {
45
+ selector: "Literal[value=/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/]",
46
+ message: "Hardcoded hex color: use a palette token."
47
+ },
48
+ "rgb-color": {
49
+ selector: "Literal[value=/^rgba?\\(/]",
50
+ message: "Hardcoded rgb()/rgba() color: use a palette token."
51
+ },
52
+ "typography-style": {
53
+ selector: `:matches(JSXAttribute[name.name=/^(sx|style)$/], CallExpression[callee.callee.name='styled'], CallExpression[callee.callee.object.name='styled']) Property[key.name=/^(fontSize|fontWeight|lineHeight|fontFamily)$/]`,
54
+ message: "Hardcoded typography: use `<Typography variant>` (and `weight`), or the theme typography tokens."
55
+ },
56
+ "external-link": {
57
+ selector: "JSXOpeningElement[name.name='a'] > JSXAttribute[name.name='target'] > Literal[value='_blank']",
58
+ message: 'Raw `<a target="_blank">`: use Meridian `Link` with `external`.'
59
+ }
60
+ };
61
+ export {
62
+ meridianDesignRules
63
+ };