Conversation
GutenbergKit's SCSS was never linted for physical left/right properties, so a direction-specific bug (border-right-color on the toolbar) had to be found by eye. Add stylelint with stylelint-plugin-logical-css, mirroring the setup and ignore list upstream Gutenberg uses in tools/stylelint/config.js, and wire lint:css / lint-css into npm scripts, the Makefile, and CI alongside lint:js / lint-js. Also converts the ~29 existing physical-property declarations the new rule flags to their logical equivalents. Two declarations in the toolbar's scroll-indicator gradients are not a symmetric pair and are left physical with a stylelint-disable, pending separate investigation into their RTL behavior. The base @wordpress/stylelint-config/scss ruleset is otherwise stricter than this codebase's existing conventions; the selector-naming, blank-line, and specificity-ordering rules it would also enable are relaxed the same way upstream's own config relaxes them, to keep this change scoped to the logical-properties rule.
|
In terms of infra/tooling ( |
|
@akashfer thank you for exploring this. It may take me a little while to begin review of this, but hopefully I can next week. I'll follow up. |
|
Okay thanks |
dcalhoun
left a comment
There was a problem hiding this comment.
@akashfer thank you for contributing this! 🙇🏻♂️
I tested the editor with the style changes. I did not encounter any regressions.
In my review with Claude, I did uncover several findings that I believe are legitimate and worth addressing. Would you please take a look at addressing the inline comments?
Lastly, if you merge the latest trunk branch into this branch, we'll receive #615, which will help avoid cryptic lint run hangs.
| // parent editor toolbar to avoid nested scrolling views. Also disable scrolling | ||
| // of the block toolbar itself, relying on the parent container scrolling instead. |
There was a problem hiding this comment.
It seems we can forgo the copy of this comment from line 119. It duplicates the existing comment.
| // parent editor toolbar to avoid nested scrolling views. Also disable scrolling | |
| // of the block toolbar itself, relying on the parent container scrolling instead. | |
| // parent editor toolbar to avoid nested scrolling views. |
| /** @type {import('stylelint').Config} */ | ||
| export default { | ||
| extends: '@wordpress/stylelint-config/scss', | ||
| plugins: ['stylelint-plugin-logical-css'], |
There was a problem hiding this comment.
Finding from Claude:
This file fails prettier --check (wp-prettier's bracket spacing). CI won't catch it: formatting is enforced via the prettier/prettier ESLint rule, and lint:js runs eslint . --ext js,jsx, so .mjs is never linted.
| plugins: ['stylelint-plugin-logical-css'], | |
| plugins: [ 'stylelint-plugin-logical-css' ], |
Worth adding mjs,cjs to the --ext list in a follow-up — .eslintrc.cjs, .prettierrc.cjs, and e2e/.eslintrc.cjs all already pass, so it'd be a no-op today.
| export default { | ||
| extends: '@wordpress/stylelint-config/scss', | ||
| plugins: ['stylelint-plugin-logical-css'], | ||
| rules: { |
There was a problem hiding this comment.
Finding from Claude:
Gutenberg's tools/stylelint/config.js sets both of these; without them the stylelint-disable-next-line comments added below can rot silently, and a stale disable will suppress whatever lands on the line after it. lint:js already enforces the equivalent via --report-unused-disable-directives.
| rules: { | |
| reportNeedlessDisables: true, | |
| reportDescriptionlessDisables: true, | |
| rules: { |
reportDescriptionlessDisables requires a -- reason on each disable directive, which pairs with the toolbar comments.
| extends: '@wordpress/stylelint-config/scss', | ||
| plugins: ['stylelint-plugin-logical-css'], | ||
| rules: { | ||
| 'plugin/use-logical-properties-and-values': [ |
There was a problem hiding this comment.
Finding from Claude:
Gutenberg is on stylelint-plugin-logical-css@^2.1.0, where this rule was renamed and split in two. Worth moving now rather than landing disable comments under a name we'd have to rename later — ^1.2.3 can't cross the major, so the pin freezes here otherwise.
I ran v2 against src/**/*.scss with this exact ignore list: the only violations are the two toolbar lines that already carry disables, flagged because v2 doesn't recognise the v1 name in them. Nothing else in the codebase changes.
| 'plugin/use-logical-properties-and-values': [ | |
| 'logical-css/require-logical-keywords': true, | |
| 'logical-css/require-logical-properties': [ |
Goes together with the version bump on package.json and the two disable directives in editor-toolbar/style.scss.
| // Doesn't affect RTL styles | ||
| 'border-bottom', | ||
| 'border-top', | ||
| 'width', | ||
| 'min-width', | ||
| 'max-width', | ||
| 'height', | ||
| 'min-height', | ||
| 'max-height', | ||
| 'margin-top', | ||
| 'margin-bottom', | ||
| 'overflow-x', | ||
| 'overflow-y', | ||
| 'padding-top', | ||
| 'padding-bottom', | ||
| 'scroll-margin-top', | ||
| 'scroll-margin-bottom', | ||
| 'top', | ||
| 'bottom', |
There was a problem hiding this comment.
Finding from Claude:
The list is copied from Gutenberg and inherits its gaps: it ignores the block-axis shorthands but not the longhands. Per the plugin's physical.js, these are also flagged and equally unable to flip in RTL:
| // Doesn't affect RTL styles | |
| 'border-bottom', | |
| 'border-top', | |
| 'width', | |
| 'min-width', | |
| 'max-width', | |
| 'height', | |
| 'min-height', | |
| 'max-height', | |
| 'margin-top', | |
| 'margin-bottom', | |
| 'overflow-x', | |
| 'overflow-y', | |
| 'padding-top', | |
| 'padding-bottom', | |
| 'scroll-margin-top', | |
| 'scroll-margin-bottom', | |
| 'top', | |
| 'bottom', | |
| // Doesn't affect RTL styles | |
| 'border-bottom', | |
| 'border-bottom-color', | |
| 'border-bottom-style', | |
| 'border-bottom-width', | |
| 'border-top', | |
| 'border-top-color', | |
| 'border-top-style', | |
| 'border-top-width', | |
| 'box-orient', | |
| 'contain-intrinsic-height', | |
| 'contain-intrinsic-width', | |
| 'width', | |
| 'min-width', | |
| 'max-width', | |
| 'height', | |
| 'min-height', | |
| 'max-height', | |
| 'margin-top', | |
| 'margin-bottom', | |
| 'overflow-x', | |
| 'overflow-y', | |
| 'overscroll-behavior-x', | |
| 'overscroll-behavior-y', | |
| 'padding-top', | |
| 'padding-bottom', | |
| 'scroll-margin-top', | |
| 'scroll-margin-bottom', | |
| 'scroll-padding-top', | |
| 'scroll-padding-bottom', | |
| 'top', | |
| 'bottom', |
(border-top-left-radius and friends stay flagged — they name a side on the inline axis.) This is what forced the scroll-padding-bottom conversion in src/index.scss.
| // Not a symmetric pair with the `right` below (see the `::after` gradient) — | ||
| // whether this should flip in RTL needs its own investigation, so it is | ||
| // intentionally left physical for now rather than converted here. | ||
| // stylelint-disable-next-line plugin/use-logical-properties-and-values |
There was a problem hiding this comment.
Finding from Claude:
The stated reason doesn't hold — this isn't pending investigation, it's a permanent constraint. left: 0 pairs with the hard-coded linear-gradient(to right) three lines below; linear-gradient has no logical direction, so converting the position alone would put the fade on the wrong edge in RTL. Both have to flip together or neither does.
| // Not a symmetric pair with the `right` below (see the `::after` gradient) — | |
| // whether this should flip in RTL needs its own investigation, so it is | |
| // intentionally left physical for now rather than converted here. | |
| // stylelint-disable-next-line plugin/use-logical-properties-and-values | |
| // stylelint-disable-next-line plugin/use-logical-properties-and-values -- paired with the gradient direction below |
The directive will need renaming to logical-css/require-logical-properties if the plugin is bumped to v2.
| // Not a symmetric pair with the `left` above — see the note on the | ||
| // `::before` gradient. | ||
| // stylelint-disable-next-line plugin/use-logical-properties-and-values |
There was a problem hiding this comment.
Finding from Claude:
Same as above — right: 0 pairs with linear-gradient(to left) below, which is the real reason this can't be converted in isolation.
| // Not a symmetric pair with the `left` above — see the note on the | |
| // `::before` gradient. | |
| // stylelint-disable-next-line plugin/use-logical-properties-and-values | |
| // stylelint-disable-next-line plugin/use-logical-properties-and-values -- paired with the gradient direction below |
The directive will need renaming to logical-css/require-logical-properties if the plugin is bumped to v2.
| "patch-package": "^8.0.1", | ||
| "postcss-scss": "^4.0.9", |
There was a problem hiding this comment.
Finding from Claude:
postcss-scss is a direct dependency of stylelint-config-recommended-scss, which requires it internally to set customSyntax. Nothing in this repo imports it, so declaring it here is dead weight we'd have to version-bump by hand.
| "patch-package": "^8.0.1", | |
| "postcss-scss": "^4.0.9", | |
| "patch-package": "^8.0.1", |
| "react-devtools-core": "^7.0.1", | ||
| "sass-embedded": "^1.98.0", | ||
| "stylelint": "^16.26.1", | ||
| "stylelint-plugin-logical-css": "^1.2.3", |
There was a problem hiding this comment.
Finding from Claude:
Bump to match Gutenberg — see the rule-name comment on .stylelintrc.mjs, which has the measured cost. Apply both together or neither; the renamed rules won't resolve on v1.
| "stylelint-plugin-logical-css": "^1.2.3", | |
| "stylelint-plugin-logical-css": "^2.1.0", |
| "sass-embedded": "^1.98.0", | ||
| "stylelint": "^16.26.1", | ||
| "stylelint-plugin-logical-css": "^1.2.3", | ||
| "vite": "^8.0.16", |
There was a problem hiding this comment.
Finding from Claude:
stylelint-scss is missing. @wordpress/stylelint-config declares it as a peer and its scss.js loads it by name (plugins: ['stylelint-scss']), so it only resolves today because npm auto-installs and hoists the peer. Under a non-hoisting installer or --legacy-peer-deps, config load fails outright.
| "vite": "^8.0.16", | |
| "stylelint-scss": "^6.4.0", | |
| "vite": "^8.0.16", |
Fixes #564
What?
Adds a stylelint setup to GutenbergKit's SCSS, using
stylelint-plugin-logical-cssto flag physical (left/right/padding-left/etc.) properties that should be written as logical properties (inset-inline-start,padding-inline, etc.) for RTL correctness. Converts the ~29 existing violations the new rule flags to their logical equivalents.Why?
GutenbergKit's own SCSS is never processed by
rtlcss, and nothing currently guards against physical-property RTL bugs — one (border-right-colormisplacing the toolbar divider) had to be found by eye during RTL testing. There was no stylelint setup at all: no config, no dependency, nolint:cssscript, no Makefile target.How?
stylelint,stylelint-plugin-logical-css(pinned to^1.2.3, matching the version upstream Gutenberg pins intools/stylelint/config.js— v2 renamed the rule this issue asks for),@wordpress/stylelint-config, andpostcss-scssas dev dependencies..stylelintrc.mjsextending@wordpress/stylelint-config/scssand enablingplugin/use-logical-properties-and-values, reusing upstream'signorelist for properties that don't affect RTL (margin-top,width,overflow-y,border-top, etc.).@wordpress/stylelint-config/scssruleset is otherwise stricter than this codebase's existing conventions (BEM-style double-underscore class names, blank-line formatting, specificity ordering). Relaxed the same rules upstream's own config relaxes for the same reason, to keep this PR scoped to the logical-properties rule rather than a repo-wide style rewrite.lint:css/lint:css:fixnpm scripts and matchinglint-css/lint-css-fixMakefile targets, mirroring the existinglint-jspattern.lint-cssinto the Buildkite pipeline alongsidelint-js, including the release-gatedepends_onlist.left/rightpairs, or single vertical properties likescroll-padding-bottom) to their logical equivalents.::before/::after) are not a symmetric pair — they're the left- and right-scroll affordances, and whether they should flip in RTL needs its own investigation. Left those physical with an inlinestylelint-disable-next-lineand a comment, rather than guessing at a fix.position: absoluteoverridden by a laterposition: fixed !important, two duplicate selector blocks that can merge into one, three0px→0unit cleanups, and a few non-shorthand/named colors).Testing Instructions
npm run lint:css(ormake lint-css) — should pass with no errors.make lint-js/make test-js/make build— included here to confirm the change doesn't affect JS lint, unit tests (227 passing), or the Vite build.src/components/editor-toolbar/style.scss) and the visual editor toolbar/error boundary (src/components/visual-editor/style.scss) in both LTR and RTL (?lang=aror similar).Accessibility Testing Instructions
No UI/behavior changes — this is a build-tooling and CSS-property-name change only; visual output is unchanged (logical properties resolve to the same physical box-model values in LTR, which is this project's current default and only tested direction).