@jetbrains/ring-ui 7.0.123-beta.0 → 7.0.124

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/README.md CHANGED
@@ -13,10 +13,6 @@
13
13
 
14
14
  This collection of UI components aims to provide all the necessary building blocks for web-based products built inside JetBrains, as well as third-party plugins developed for JetBrains' products.
15
15
 
16
- ## Guidance for coding agents
17
-
18
- The npm packages include [`skills/ring-ui/SKILL.md`](./skills/ring-ui/SKILL.md), which teaches coding agents to consume Ring UI through `@jetbrains/ring-ui-built` when composing complete pages, responsive layouts, forms, themes, and feedback. The same Skill files are published in both npm artifacts.
19
-
20
16
  ## Try now
21
17
  * Try the [codesandbox](https://codesandbox.io/p/sandbox/ring-ui-7-0-demo-z6v6ym), based on `create-react-app` tooling, to see and try the UI components
22
18
  * Check out [list of examples](https://jetbrains.github.io/ring-ui/master/index.html) for each component
@@ -1,6 +1,6 @@
1
1
  @import '../global/variables.css';
2
2
 
3
- @value button, active as buttonActive, flat from '../button/button.css';
3
+ @value button as buttonClass, active as buttonActive, flat from '../button/button.css';
4
4
 
5
5
  :root,
6
6
  :host {
@@ -37,7 +37,7 @@
37
37
  --ring-button-group-button-border-color: var(--ring-border-disabled-color);
38
38
  }
39
39
 
40
- .buttonGroup .button {
40
+ .buttonGroup .buttonClass {
41
41
  position: relative;
42
42
 
43
43
  transition: none;
@@ -47,7 +47,7 @@
47
47
  0 -1px var(--ring-button-group-button-border-color) inset;
48
48
  }
49
49
 
50
- .buttonGroup .button[disabled] {
50
+ .buttonGroup .buttonClass[disabled] {
51
51
  /* stylelint-disable selector-max-specificity */
52
52
  &::before,
53
53
  &::after {
@@ -75,8 +75,8 @@
75
75
  /* stylelint-enable */
76
76
  }
77
77
 
78
- .buttonGroup > .button:first-child,
79
- .buttonGroup > :first-child .button {
78
+ .buttonGroup > .buttonClass:first-child,
79
+ .buttonGroup > :first-child .buttonClass {
80
80
  box-shadow:
81
81
  0 1px var(--ring-button-group-button-border-color) inset,
82
82
  0 -1px var(--ring-button-group-button-border-color) inset,
@@ -88,8 +88,8 @@
88
88
  }
89
89
  }
90
90
 
91
- .buttonGroup > .button:last-child,
92
- .buttonGroup > :last-child .button {
91
+ .buttonGroup > .buttonClass:last-child,
92
+ .buttonGroup > :last-child .buttonClass {
93
93
  box-shadow:
94
94
  0 1px var(--ring-button-group-button-border-color) inset,
95
95
  0 -1px var(--ring-button-group-button-border-color) inset,
@@ -101,8 +101,8 @@
101
101
  }
102
102
  }
103
103
 
104
- .buttonGroup > .button:only-child,
105
- .buttonGroup > :only-child .button {
104
+ .buttonGroup > .buttonClass:only-child,
105
+ .buttonGroup > :only-child .buttonClass {
106
106
  box-shadow:
107
107
  0 1px var(--ring-button-group-button-border-color) inset,
108
108
  0 -1px var(--ring-button-group-button-border-color) inset,
@@ -116,15 +116,15 @@
116
116
  }
117
117
 
118
118
  /* stylelint-disable selector-max-specificity */
119
- .buttonGroup .button.button:hover:not(:disabled),
120
- .buttonGroup .button.button:active:not(:disabled) {
119
+ .buttonGroup .buttonClass.buttonClass:hover:not(:disabled),
120
+ .buttonGroup .buttonClass.buttonClass:active:not(:disabled) {
121
121
  --ring-button-border-radius-left: var(--ring-border-radius);
122
122
  --ring-button-border-radius-right: var(--ring-border-radius);
123
123
 
124
124
  box-shadow: var(--ring-button-shadow) var(--ring-button-border-color);
125
125
  }
126
126
 
127
- .buttonGroup .button.button:focus-visible {
127
+ .buttonGroup .buttonClass.buttonClass:focus-visible {
128
128
  --ring-button-border-radius-left: var(--ring-border-radius);
129
129
  --ring-button-border-radius-right: var(--ring-border-radius);
130
130
 
@@ -133,14 +133,14 @@
133
133
  0 0 0 1px var(--ring-border-hover-color);
134
134
  }
135
135
 
136
- .buttonGroup .button.button.buttonActive {
136
+ .buttonGroup .buttonClass.buttonClass.buttonActive {
137
137
  --ring-button-border-radius-left: var(--ring-border-radius);
138
138
  --ring-button-border-radius-right: var(--ring-border-radius);
139
139
 
140
140
  box-shadow: var(--ring-button-shadow) var(--ring-button-border-color);
141
141
  }
142
142
 
143
- .buttonGroup .button:focus-visible.buttonActive {
143
+ .buttonGroup .buttonClass:focus-visible.buttonActive {
144
144
  --ring-button-border-radius-left: var(--ring-border-radius);
145
145
  --ring-button-border-radius-right: var(--ring-border-radius);
146
146
 
@@ -149,7 +149,7 @@
149
149
  0 0 0 1px var(--ring-border-hover-color);
150
150
  }
151
151
 
152
- .buttonGroup .button.buttonActive[disabled] {
152
+ .buttonGroup .buttonClass.buttonActive[disabled] {
153
153
  box-shadow: var(--ring-button-shadow) var(--ring-border-hover-color);
154
154
  }
155
155
  /* stylelint-enable */
@@ -159,17 +159,17 @@
159
159
  composes: buttonGroup from '../button-toolbar/button-toolbar.css';
160
160
  }
161
161
 
162
- .common button {
162
+ .common button:not(:where(.buttonClass)) {
163
163
  border-radius: 0;
164
164
  }
165
165
 
166
- .common .button {
166
+ .common .buttonClass {
167
167
  --ring-button-border-radius-left: 0;
168
168
  --ring-button-border-radius-right: 0;
169
169
  }
170
170
 
171
171
  .split button,
172
- .split .button {
172
+ .split .buttonClass {
173
173
  margin: 0 0 0 -1px;
174
174
  }
175
175
 
@@ -192,8 +192,8 @@
192
192
  border-bottom-left-radius: var(--ring-border-radius);
193
193
  }
194
194
 
195
- .common > .button:first-child,
196
- .common > :first-child .button {
195
+ .common > .buttonClass:first-child,
196
+ .common > :first-child .buttonClass {
197
197
  --ring-button-border-radius-left: var(--ring-border-radius);
198
198
 
199
199
  margin: 0;
@@ -204,8 +204,8 @@
204
204
  border-bottom-right-radius: var(--ring-border-radius);
205
205
  }
206
206
 
207
- .common > .button:last-child,
208
- .common > :last-child .button {
207
+ .common > .buttonClass:last-child,
208
+ .common > :last-child .buttonClass {
209
209
  --ring-button-border-radius-right: var(--ring-border-radius);
210
210
  }
211
211
 
@@ -218,7 +218,7 @@
218
218
  }
219
219
 
220
220
  .common {
221
- & .button {
221
+ & .buttonClass {
222
222
  position: relative;
223
223
  z-index: var(--ring-button-group-default-z-index);
224
224
 
@@ -1,6 +1,6 @@
1
1
  @import '../global/variables.css';
2
2
 
3
- @value button from '../button/button.css';
3
+ @value button as buttonClass from '../button/button.css';
4
4
 
5
5
  .buttonToolbar {
6
6
  display: inline-block;
@@ -13,7 +13,7 @@
13
13
 
14
14
  .buttonToolbar > button,
15
15
  .buttonToolbar > :global(.ring-button-group),
16
- .buttonToolbar > .button,
16
+ .buttonToolbar > .buttonClass,
17
17
  .buttonToolbar > .buttonGroup,
18
18
  .buttonToolbar > .split,
19
19
  .buttonToolbar > .buttonToolbar {
@@ -1,5 +1,6 @@
1
1
  import { Component } from 'react';
2
2
  import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { getTrustedHTML } from '../global/configuration';
3
4
  /**
4
5
  * @name ContentEditable
5
6
  */
@@ -23,7 +24,7 @@ class ContentEditableBase extends Component {
23
24
  return (<div {...props} ref={inputRef}
24
25
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
26
  // @ts-ignore
26
- disabled={disabled} role='textbox' tabIndex={disabled ? undefined : tabIndex} contentEditable={!this.props.disabled} dangerouslySetInnerHTML={{ __html }}/>);
27
+ disabled={disabled} role='textbox' tabIndex={disabled ? undefined : tabIndex} contentEditable={!this.props.disabled} dangerouslySetInnerHTML={{ __html: getTrustedHTML(__html) }}/>);
27
28
  }
28
29
  }
29
30
  const ContentEditable = ({ children, ...props }) => (<ContentEditableBase {...props} __html={renderToStaticMarkup(children)}/>);
@@ -3,9 +3,14 @@ export declare enum ControlsHeight {
3
3
  M = "M",
4
4
  L = "L"
5
5
  }
6
+ export interface RingUITrustedTypePolicy {
7
+ createHTML(html: string): TrustedHTML;
8
+ }
6
9
  export interface RingUIConfiguration {
7
10
  controlsHeight?: ControlsHeight;
8
11
  popupsCssPositioning?: boolean;
12
+ trustedTypePolicy?: RingUITrustedTypePolicy | null;
9
13
  }
10
14
  export declare function configure(config: RingUIConfiguration): void;
11
15
  export declare function getConfiguration(): Required<RingUIConfiguration>;
16
+ export declare function getTrustedHTML(html: string): string | TrustedHTML;
@@ -7,6 +7,7 @@ export var ControlsHeight;
7
7
  const globalConfiguration = {
8
8
  controlsHeight: ControlsHeight.M,
9
9
  popupsCssPositioning: true,
10
+ trustedTypePolicy: null,
10
11
  };
11
12
  export function configure(config) {
12
13
  if (config.controlsHeight !== undefined) {
@@ -15,7 +16,13 @@ export function configure(config) {
15
16
  if (config.popupsCssPositioning !== undefined) {
16
17
  globalConfiguration.popupsCssPositioning = config.popupsCssPositioning;
17
18
  }
19
+ if (config.trustedTypePolicy !== undefined) {
20
+ globalConfiguration.trustedTypePolicy = config.trustedTypePolicy;
21
+ }
18
22
  }
19
23
  export function getConfiguration() {
20
24
  return { ...globalConfiguration };
21
25
  }
26
+ export function getTrustedHTML(html) {
27
+ return globalConfiguration.trustedTypePolicy?.createHTML(html) ?? html;
28
+ }
@@ -1,6 +1,7 @@
1
1
  // Inspired by https://github.com/sairion/svg-inline-react
2
2
  import { memo } from 'react';
3
3
  import classNames from 'classnames';
4
+ import { getTrustedHTML } from '../global/configuration';
4
5
  import memoize from '../global/memoize';
5
6
  import styles from './icon.css';
6
7
  function convertReactSVGDOMProperty(str) {
@@ -27,7 +28,7 @@ function extractSVGProps(svgNode) {
27
28
  }
28
29
  const getSVGFromSource = memoize((src) => {
29
30
  const svgContainer = document.createElement('div');
30
- svgContainer.innerHTML = src;
31
+ svgContainer.innerHTML = getTrustedHTML(src);
31
32
  const svg = svgContainer.firstElementChild;
32
33
  if (svg.remove) {
33
34
  svg.remove();
@@ -51,7 +52,7 @@ function IconSVG({ src, className, ...rest }) {
51
52
  }, className);
52
53
  const { props, html } = getSVGFromSource(src);
53
54
  return (<svg {...props} {...rest} className={glyphClasses} dangerouslySetInnerHTML={{
54
- __html: html,
55
+ __html: getTrustedHTML(html),
55
56
  }}/>);
56
57
  }
57
58
  export default memo(IconSVG);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui",
3
- "version": "7.0.123-beta.0",
3
+ "version": "7.0.124",
4
4
  "description": "JetBrains UI library",
5
5
  "author": {
6
6
  "name": "JetBrains"
@@ -11,7 +11,6 @@
11
11
  "babel.config.js",
12
12
  "components",
13
13
  "postcss.config.js",
14
- "skills",
15
14
  "webpack.config.js",
16
15
  "typings.d.ts"
17
16
  ],
@@ -60,7 +59,7 @@
60
59
  "prelint-ci": "echo \"##teamcity[importData type='jslint' path='eslint-report.xml']\"",
61
60
  "lint-ci": "eslint --format jslint-xml . > eslint-report.xml && npm run stylelint-ci",
62
61
  "lint:js": "eslint",
63
- "postbuild": "cpy './**/*.d.ts' ../dist --parents --cwd=components/",
62
+ "postbuild": "cpy './**/*.d.ts' ../dist --parents --cwd=components/ && node scripts/check-built-css.mjs",
64
63
  "_postinstall": "husky && npm run postinstall:gitconfig",
65
64
  "postinstall:gitconfig": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
66
65
  "postpublish": "pinst --enable",
@@ -69,7 +68,7 @@
69
68
  "prebuild": "rimraf components && npm run prebuild:tsc && cpy './**/*' '!**/*.stories.*' '!**/*.figma.*' '!**/*.ts' '!**/*.tsx' '!**/test-helpers/mocks/**' ../components --parents --cwd=src/",
70
69
  "prebuild:tsc": "(npm run type-check:create-d-ts && tsc --project tsconfig-build.json) ; npm run type-check:cleanup-d-ts",
71
70
  "prepare": "webpack -c .storybook/custom-header/webpack.config.js",
72
- "prepublishOnly": "npm run verify-package-skills && pinst --disable",
71
+ "prepublishOnly": "pinst --disable",
73
72
  "prerelease-built-ci": "node scripts/prepare-built-package.js",
74
73
  "prerelease-ci": "git pull",
75
74
  "release-built-ci": "npm publish --access=public $NPM_PUBLISH_PARAMS",
@@ -83,8 +82,7 @@
83
82
  "storybook-debug": "node --inspect-brk node_modules/@storybook/react/bin -p 9999",
84
83
  "stylelint": "stylelint --ignore-path .stylelintignore '**/*.css'",
85
84
  "stylelint-ci": "stylelint --ignore-path .stylelintignore --custom-formatter 'scripts/jslint-xml.js' '**/*.css' | xmlappend eslint-report.xml",
86
- "test": "vitest src",
87
- "verify-package-skills": "node scripts/verify-package-skills.mjs",
85
+ "test": "vitest src && npm test --workspace @jetbrains/rollup-css-plugin",
88
86
  "type-check": "(npm run type-check:create-d-ts && npm run type-check:main && npm run type-check:build) ; npm run type-check:cleanup-d-ts",
89
87
  "type-check:create-d-ts": "npx tcm src && npx tcm .storybook",
90
88
  "type-check:main": "tsc --noEmit -p tsconfig.json",
@@ -108,41 +106,41 @@
108
106
  "@eslint/compat": "^2.1.0",
109
107
  "@eslint/eslintrc": "^3.3.6",
110
108
  "@eslint/js": "^10.0.1",
111
- "@figma/code-connect": "^1.4.9",
109
+ "@figma/code-connect": "^1.5.2",
112
110
  "@jetbrains/eslint-config": "^6.0.5",
113
111
  "@jetbrains/logos": "3.0.0-canary.734b213.0",
114
112
  "@jetbrains/rollup-css-plugin": "./packages/rollup-css-plugin",
115
113
  "@jetbrains/stylelint-config": "^4.0.2",
116
114
  "@jetbrains/typescript-plugin-css-modules": "^5.3.1",
117
- "@primer/octicons": "^19.29.2",
115
+ "@primer/octicons": "^19.33.0",
118
116
  "@rollup/plugin-babel": "^7.1.0",
119
117
  "@rollup/plugin-json": "^6.1.0",
120
118
  "@rollup/plugin-node-resolve": "^16.0.3",
121
119
  "@rollup/plugin-replace": "^6.0.3",
122
- "@storybook/addon-a11y": "10.5.4",
123
- "@storybook/addon-docs": "^10.5.4",
124
- "@storybook/addon-themes": "^10.5.4",
120
+ "@storybook/addon-a11y": "10.5.7",
121
+ "@storybook/addon-docs": "^10.5.7",
122
+ "@storybook/addon-themes": "^10.5.7",
125
123
  "@storybook/csf": "^0.1.13",
126
- "@storybook/react-webpack5": "10.5.4",
124
+ "@storybook/react-webpack5": "10.5.7",
127
125
  "@storybook/test-runner": "^0.24.4",
128
126
  "@testing-library/dom": "^10.4.1",
129
127
  "@testing-library/react": "^16.3.2",
130
- "@testing-library/user-event": "^14.6.1",
128
+ "@testing-library/user-event": "^14.6.3",
131
129
  "@types/chai-as-promised": "^8.0.2",
132
130
  "@types/chai-dom": "1.11.3",
133
131
  "@types/markdown-it": "^14.1.2",
134
- "@types/react": "^19.2.17",
135
- "@types/react-dom": "^19.2.3",
132
+ "@types/react": "^19.2.18",
133
+ "@types/react-dom": "^19.2.4",
136
134
  "@types/webpack-env": "^1.18.8",
137
135
  "@vitejs/plugin-react": "^6.0.4",
138
136
  "@vitest/eslint-plugin": "^1.6.24",
139
- "acorn": "^8.17.0",
137
+ "acorn": "^8.18.0",
140
138
  "babel-plugin-require-context-hook": "^1.0.0",
141
139
  "caniuse-lite": "^1.0.30001799",
142
140
  "chai-as-promised": "^8.0.2",
143
141
  "chai-dom": "^1.12.1",
144
142
  "cheerio": "^1.2.0",
145
- "core-js": "^3.49.0",
143
+ "core-js": "^3.50.0",
146
144
  "cpy-cli": "^7.0.0",
147
145
  "dotenv-cli": "^11.0.0",
148
146
  "eslint": "^9.39.5",
@@ -156,12 +154,12 @@
156
154
  "eslint-plugin-prettier": "^5.5.6",
157
155
  "eslint-plugin-react": "^7.37.5",
158
156
  "eslint-plugin-react-hooks": "^7.1.1",
159
- "eslint-plugin-storybook": "^10.5.4",
157
+ "eslint-plugin-storybook": "^10.5.7",
160
158
  "eslint-plugin-unicorn": "^72.0.0",
161
159
  "events": "^3.3.0",
162
160
  "glob": "^13.0.6",
163
- "globals": "^17.7.0",
164
- "html-webpack-plugin": "^5.6.7",
161
+ "globals": "^17.9.0",
162
+ "html-webpack-plugin": "^5.6.8",
165
163
  "http-server": "^14.1.1",
166
164
  "husky": "^9.1.7",
167
165
  "identity-obj-proxy": "^3.0.0",
@@ -169,19 +167,19 @@
169
167
  "jest-environment-jsdom": "^30.4.1",
170
168
  "jest-teamcity": "^1.12.0",
171
169
  "lint-staged": "^17.2.0",
172
- "markdown-it": "^14.3.0",
170
+ "markdown-it": "^15.0.0",
173
171
  "merge-options": "^3.0.4",
174
172
  "pinst": "^3.0.0",
175
173
  "prettier": "^3.9.6",
176
174
  "raw-loader": "^4.0.2",
177
- "react": "^19.2.7",
178
- "react-dom": "^19.2.7",
175
+ "react": "^19.2.8",
176
+ "react-dom": "^19.2.8",
179
177
  "regenerator-runtime": "^0.14.1",
180
178
  "rimraf": "^6.1.3",
181
- "rollup": "^4.62.2",
179
+ "rollup": "^4.62.4",
182
180
  "rollup-plugin-clear": "^2.0.7",
183
181
  "storage-mock": "^2.1.0",
184
- "storybook": "10.5.4",
182
+ "storybook": "10.5.7",
185
183
  "stylelint": "^17.14.1",
186
184
  "stylelint-config-sass-guidelines": "^13.0.0",
187
185
  "svg-inline-loader": "^0.8.2",
@@ -190,11 +188,11 @@
190
188
  "typed-css-modules": "^0.9.1",
191
189
  "typescript": "~6.0.3",
192
190
  "typescript-eslint": "^8.65.0",
193
- "vite": "^8.1.4",
191
+ "vite": "^8.2.1",
194
192
  "vitest": "^4.1.10",
195
193
  "vitest-teamcity-reporter": "^0.6.1",
196
- "webpack": "^5.108.4",
197
- "webpack-cli": "^7.2.1",
194
+ "webpack": "^5.109.2",
195
+ "webpack-cli": "^7.2.2",
198
196
  "xmlappend": "^1.0.4"
199
197
  },
200
198
  "peerDependencies": {
@@ -229,8 +227,8 @@
229
227
  "babel-loader": "10.1.1",
230
228
  "babel-plugin-react-compiler": "^1.0.0",
231
229
  "babel-plugin-transform-define": "^2.1.4",
232
- "browserslist": "^4.28.6",
233
- "change-case": "^4.1.1",
230
+ "browserslist": "^4.28.8",
231
+ "change-case": "^5.4.4",
234
232
  "classnames": "^2.5.1",
235
233
  "combokeys": "^3.0.1",
236
234
  "css-loader": "^7.1.4",
@@ -244,7 +242,7 @@
244
242
  "highlight.js": "^10.7.2",
245
243
  "just-debounce-it": "^3.2.0",
246
244
  "memoize-one": "^6.0.0",
247
- "postcss": "^8.5.17",
245
+ "postcss": "^8.5.26",
248
246
  "postcss-calc": "^10.1.1",
249
247
  "postcss-font-family-system-ui": "^5.0.0",
250
248
  "postcss-loader": "^8.2.1",
@@ -1,43 +0,0 @@
1
- ---
2
- name: ring-ui
3
- description: Use when implementing, laying out, theming, validating, or troubleshooting React interfaces that use @jetbrains/ring-ui-built.
4
- ---
5
-
6
- # Ring UI
7
-
8
- Use Ring UI for controls, surfaces, and design tokens. Let semantic HTML and native CSS Flexbox/Grid own page structure and geometry.
9
-
10
- ## When to use
11
-
12
- Use this skill for React pages, settings forms, dialogs, navigation, responsive composition, themes, loading/empty/error states, or accessibility work in a project that uses or is being set up to use `@jetbrains/ring-ui-built`.
13
-
14
- Do not use it for non-React interfaces, projects that neither use nor are being set up to use Ring UI, or product-specific YouTrack infrastructure. Do not introduce Ring UI merely to solve generic page layout.
15
-
16
- ## Workflow
17
-
18
- 1. Before initializing or laying out an application, read [references/page-layouts.md](references/page-layouts.md) and follow its app setup, even if `@jetbrains/ring-ui-built` is not installed yet.
19
- 2. If the package is installed, inspect `package.json` and the lockfile for its exact version. Read its `README.md` and relevant `.d.ts` files before choosing imports or props.
20
- 3. Import components only from `@jetbrains/ring-ui-built`. Compose Ring controls and surfaces inside semantic landmarks, and use native CSS Flexbox/Grid for page geometry and CSS media/container queries for responsiveness.
21
- 4. Use `--ring-*` tokens for colors, typography, borders, and spacing based on `--ring-unit`. Do not copy their resolved values.
22
- 5. Model form draft, saved, validation, submission, and result state explicitly. Distinguish blocking loading, local loading, background work, first-use empty, filtered empty, success, warning, and retryable failure.
23
-
24
- ## Preferred patterns
25
-
26
- - Use one semantic `h1`, clear landmarks, native anchors for navigation, and native/Ring buttons for actions. Do not make clickable `div` elements.
27
- - Prefer `Input`'s own `label`. Give externally labelled controls stable IDs and connect `htmlFor`, `aria-describedby`, and errors explicitly.
28
- - Keep `Select` controlled and explicitly generic: use `Select<T>` with `SelectItem<T>[]` data, selected item, and `onChange` argument—never `Select<SelectItem<T>>` or guessed primitive values.
29
- - Wrap error `Banner` content in `role="alert"`, non-urgent status/success content in `role="status"`, blocking `LoaderScreen` content in a named status region, and local `LoaderInline` content in a named status region. Avoid announcing background work that does not affect the user.
30
- - Use CSS Modules. Put `min-width: 0` on shrinking grid/flex children, `min-height: 0` where vertical flex children scroll, and assign each axis to one explicit scroll owner.
31
- - Use CSS media queries for page-wide geometry and container queries for reusable components. Branch in React only when interaction or content structure must change.
32
- - Never style Ring UI internal classes or `data-test` attributes. Treat test hooks as behavior selectors, not CSS APIs.
33
-
34
- Do not add new uses of Ring UI's deprecated `Grid`/`Row`/`Col` or `ContentLayout`. Do not copy product-specific YouTrack wrappers such as `ButtonLegacy` or `IconLegacy`, page shells, portals, sticky stacks, form wrappers, or alert services into package consumers.
35
-
36
- ## Common tasks
37
-
38
- - App setup, page shells, responsive navigation, sticky regions, and scroll ownership: read [references/page-layouts.md](references/page-layouts.md).
39
- - Forms, validation, dirty state, async saving, and page feedback: read [references/forms-and-feedback.md](references/forms-and-feedback.md).
40
-
41
- ## Validation
42
-
43
- Run the consuming repository's type-check, focused tests, lint, and production build equivalents. Interactively check near `1280x800` and `390x844`: accessible names and keyboard order, every state transition and action, page and popup overflow, heading hierarchy, theme behavior, and the browser console.
@@ -1,261 +0,0 @@
1
- # Forms and feedback
2
-
3
- Use a native `<form>` as the composition root. Ring UI supplies controls and feedback surfaces; React owns the saved snapshot, draft, validation, submission, and result states.
4
-
5
- The example and consumer imports use `@jetbrains/ring-ui-built`.
6
-
7
- ## Contents
8
-
9
- - [Complete controlled settings form](#complete-controlled-settings-form)
10
- - [Form-state rules](#form-state-rules)
11
- - [Page-state decisions](#page-state-decisions)
12
-
13
- ## Complete controlled settings form
14
-
15
- `project-settings-form.tsx`:
16
-
17
- ```tsx
18
- import {useState, type FormEvent} from 'react';
19
- import Banner from '@jetbrains/ring-ui-built/components/banner/banner';
20
- import Button from '@jetbrains/ring-ui-built/components/button/button';
21
- import ButtonSet from '@jetbrains/ring-ui-built/components/button-set/button-set';
22
- import ControlLabel from '@jetbrains/ring-ui-built/components/control-label/control-label';
23
- import Input from '@jetbrains/ring-ui-built/components/input/input';
24
- import Select, {type SelectItem} from '@jetbrains/ring-ui-built/components/select/select';
25
- import Toggle from '@jetbrains/ring-ui-built/components/toggle/toggle';
26
-
27
- import styles from './project-settings-form.module.css';
28
-
29
- type Visibility = 'private' | 'team' | 'public';
30
-
31
- type VisibilityData = {
32
- value: Visibility;
33
- };
34
-
35
- type Settings = {
36
- name: string;
37
- visibility: Visibility;
38
- notifications: boolean;
39
- };
40
-
41
- type ProjectSettingsFormProps = {
42
- initialValue: Settings;
43
- onSave: (value: Settings) => Promise<void>;
44
- };
45
-
46
- const VISIBILITY_OPTIONS: SelectItem<VisibilityData>[] = [
47
- {key: 'private', label: 'Private', value: 'private'},
48
- {key: 'team', label: 'Team', value: 'team'},
49
- {key: 'public', label: 'Public', value: 'public'},
50
- ];
51
-
52
- const NAME_ID = 'project-settings-name';
53
- const NAME_HELP_ID = 'project-settings-name-help';
54
- const NAME_ERROR_ID = 'project-settings-name-error';
55
- const VISIBILITY_ID = 'project-settings-visibility';
56
- const NOTIFICATIONS_ID = 'project-settings-notifications';
57
-
58
- export function ProjectSettingsForm({initialValue, onSave}: ProjectSettingsFormProps) {
59
- const [saved, setSaved] = useState(initialValue);
60
- const [draft, setDraft] = useState(initialValue);
61
- const [submitting, setSubmitting] = useState(false);
62
- const [showErrors, setShowErrors] = useState(false);
63
- const [savedMessage, setSavedMessage] = useState<string | null>(null);
64
- const [saveError, setSaveError] = useState<string | null>(null);
65
-
66
- const nameError = draft.name.trim() ? null : 'Project name is required.';
67
- const dirty =
68
- draft.name !== saved.name ||
69
- draft.visibility !== saved.visibility ||
70
- draft.notifications !== saved.notifications;
71
- const selectedVisibility =
72
- VISIBILITY_OPTIONS.find(option => option.value === draft.visibility) ?? null;
73
-
74
- const updateDraft = (next: Settings) => {
75
- setDraft(next);
76
- setSavedMessage(null);
77
- setSaveError(null);
78
- };
79
-
80
- const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
81
- event.preventDefault();
82
- if (submitting) return;
83
-
84
- setShowErrors(true);
85
- setSavedMessage(null);
86
- setSaveError(null);
87
- if (nameError) return;
88
-
89
- const next = {...draft, name: draft.name.trim()};
90
- setSubmitting(true);
91
- try {
92
- await onSave(next);
93
- setSaved(next);
94
- setDraft(next);
95
- setShowErrors(false);
96
- setSavedMessage('Project settings saved.');
97
- } catch {
98
- setSaveError('Could not save project settings. Try again.');
99
- } finally {
100
- setSubmitting(false);
101
- }
102
- };
103
-
104
- const handleCancel = () => {
105
- setDraft(saved);
106
- setShowErrors(false);
107
- setSavedMessage(null);
108
- setSaveError(null);
109
- };
110
-
111
- const nameDescribedBy =
112
- showErrors && nameError ? `${NAME_HELP_ID} ${NAME_ERROR_ID}` : NAME_HELP_ID;
113
-
114
- return (
115
- <form className={styles.form} onSubmit={handleSubmit} noValidate>
116
- {saveError && (
117
- <div role="alert">
118
- <Banner mode="error" withIcon>
119
- {saveError}
120
- </Banner>
121
- </div>
122
- )}
123
- {savedMessage && (
124
- <div role="status">
125
- <Banner mode="success" withIcon>
126
- {savedMessage}
127
- </Banner>
128
- </div>
129
- )}
130
-
131
- <div className={styles.field}>
132
- <Input
133
- id={NAME_ID}
134
- label="Project name"
135
- value={draft.name}
136
- required
137
- disabled={submitting}
138
- aria-invalid={showErrors && Boolean(nameError)}
139
- aria-describedby={nameDescribedBy}
140
- error={showErrors && nameError ? '' : undefined}
141
- onChange={event => updateDraft({...draft, name: event.currentTarget.value})}
142
- />
143
- <p id={NAME_HELP_ID} className={styles.help}>
144
- Shown in project navigation and search.
145
- </p>
146
- {showErrors && nameError && (
147
- <p id={NAME_ERROR_ID} className={styles.error}>
148
- {nameError}
149
- </p>
150
- )}
151
- </div>
152
-
153
- <div className={styles.field}>
154
- <Select<VisibilityData>
155
- id={VISIBILITY_ID}
156
- data={VISIBILITY_OPTIONS}
157
- selected={selectedVisibility}
158
- selectedLabel="Visibility"
159
- label="Choose visibility"
160
- disabled={submitting}
161
- onChange={(option: SelectItem<VisibilityData> | null) => {
162
- if (option) updateDraft({...draft, visibility: option.value});
163
- }}
164
- />
165
- <p className={styles.help}>Controls who can discover and open this project.</p>
166
- </div>
167
-
168
- <div className={styles.toggleField}>
169
- <ControlLabel htmlFor={NOTIFICATIONS_ID} disabled={submitting}>
170
- Email notifications
171
- </ControlLabel>
172
- <Toggle
173
- id={NOTIFICATIONS_ID}
174
- checked={draft.notifications}
175
- disabled={submitting}
176
- onChange={event => updateDraft({...draft, notifications: event.currentTarget.checked})}
177
- />
178
- </div>
179
-
180
- {dirty && (
181
- <ButtonSet className={styles.actions}>
182
- <Button primary type="submit" loader={submitting} disabled={submitting}>
183
- Save
184
- </Button>
185
- <Button type="button" disabled={submitting} onClick={handleCancel}>
186
- Cancel
187
- </Button>
188
- </ButtonSet>
189
- )}
190
- </form>
191
- );
192
- }
193
- ```
194
-
195
- `project-settings-form.module.css`:
196
-
197
- ```css
198
- .form {
199
- display: grid;
200
- width: min(100%, calc(var(--ring-unit) * 75));
201
- gap: calc(var(--ring-unit) * 3);
202
- }
203
-
204
- .field {
205
- display: grid;
206
- gap: calc(var(--ring-unit) / 2);
207
- min-width: 0;
208
- }
209
-
210
- .help,
211
- .error {
212
- margin: 0;
213
- font-size: var(--ring-font-size-smaller);
214
- line-height: var(--ring-line-height-lowest);
215
- }
216
-
217
- .help {
218
- color: var(--ring-secondary-color);
219
- }
220
-
221
- .error {
222
- color: var(--ring-error-color);
223
- }
224
-
225
- .toggleField {
226
- display: flex;
227
- align-items: center;
228
- justify-content: space-between;
229
- gap: calc(var(--ring-unit) * 2);
230
- padding-block: var(--ring-unit);
231
- }
232
-
233
- .actions {
234
- padding-block-start: calc(var(--ring-unit) * 2);
235
- border-block-start: 1px solid var(--ring-line-color);
236
- }
237
- ```
238
-
239
- ## Form-state rules
240
-
241
- - Keep persisted `saved` data separate from editable `draft`. Derive validity, dirty state, and the selected `SelectItem` instead of storing duplicates.
242
- - React only uses `initialValue` for the first `useState` call. If the mounted form can switch records or receive a server refresh, resynchronize `saved` and `draft` from a stable record ID/version. Decide explicitly whether a dirty draft wins, is discarded, or requires confirmation; do not reset on every new object reference.
243
- - Add an unsaved-navigation guard when leaving the route, closing a sidebar, or dismissing a dialog could destroy a dirty draft. Remove the guard after save or cancel.
244
- - Keep field rules close to their derived errors. Reveal errors on submit or blur, retain persistent help, connect all help/error IDs with `aria-describedby`, and move focus to the first invalid field when long forms can fail off-screen.
245
- - Prevent duplicate submission in both the handler and controls. While saving, propagate `disabled` to every editable control, external label/help action, Save, Cancel, and dependent section; use the primary button's `loader` for local progress.
246
- - Put actions after page fields for ordinary pages, in the owning sidebar's sticky/footer region for sidebar editors, and in the dialog action area for dialogs. Keep them inside the native form so Enter submits predictably.
247
-
248
- ## Page-state decisions
249
-
250
- | State | Use | Accessibility and behavior |
251
- | --- | --- | --- |
252
- | Blocking loading | `LoaderScreen` in the main region | Give the region `role="status"` and an accessible loading name; replace it with content when ready. |
253
- | Local loading | `LoaderInline` beside the affected section/control | Name the local status; keep unrelated content and actions usable. |
254
- | Background loading | Subtle inline progress only when users need it | Do not block the page or repeatedly announce silent refreshes. |
255
- | Initial empty | Purpose, short explanation, and primary creation/setup action | Distinguish absence of data from an error. |
256
- | Filtered empty | “No matches” plus clear/change-filter action | Preserve the user's data and current filter context. |
257
- | Contextual risk | Persistent warning/error `Banner` near the affected controls | Use `role="alert"` only when immediate interruption is warranted. |
258
- | Success | `Banner` or message in `role="status"` near the saved region | Confirm the completed action without moving focus. |
259
- | Retryable failure | Error `Banner` in `role="alert"` with a retry path | Preserve the draft, stop the loader, and re-enable controls. |
260
-
261
- Avoid uncontrolled forms that also maintain a shared saved snapshot, untyped `Select` values or guessed primitive selections, duplicate submission paths, inaccessible labels/help, conflating first-use and filtered empty states, and transient alerts as a replacement for persistent field validation.
@@ -1,324 +0,0 @@
1
- # Page layouts
2
-
3
- Ring UI supplies controls, surfaces, and tokens. It does not replace page geometry: semantic HTML defines the document, while native CSS Grid/Flexbox defines the shell, widths, responsive behavior, sticky regions, and scroll ownership.
4
-
5
- The examples and consumer imports use `@jetbrains/ring-ui-built`.
6
-
7
- ## Contents
8
-
9
- - [App setup](#app-setup)
10
- - [Complete page shell](#complete-page-shell)
11
- - [Geometry rules](#geometry-rules)
12
- - [Sticky offsets and nested scopes](#sticky-offsets-and-nested-scopes)
13
- - [Theme, semantics, and accessibility](#theme-semantics-and-accessibility)
14
-
15
- ## App setup
16
-
17
- Use the repository's package manager to install the built package. For example, with npm:
18
-
19
- ```shell
20
- npm install @jetbrains/ring-ui-built
21
- ```
22
-
23
- Import the consolidated stylesheet exactly once at the application entry point. Import components from the same package, and put `ThemeProvider` near the application root with `Theme.AUTO` and `passToPopups`:
24
-
25
- ```tsx
26
- import {createRoot} from 'react-dom/client';
27
- import '@jetbrains/ring-ui-built/components/style.css';
28
- import Theme, {ThemeProvider} from '@jetbrains/ring-ui-built/components/global/theme';
29
-
30
- import App from './App';
31
-
32
- const rootElement = document.getElementById('root');
33
- if (!rootElement) {
34
- throw new Error('Root element #root was not found.');
35
- }
36
-
37
- const root = createRoot(rootElement);
38
-
39
- root.render(
40
- <ThemeProvider theme={Theme.AUTO} passToPopups target={document.body}>
41
- <App />
42
- </ThemeProvider>,
43
- );
44
- ```
45
-
46
- Add `class="plugin"` to `<body>`:
47
-
48
- ```html
49
- <body class="plugin">
50
- <div id="root"></div>
51
- </body>
52
- ```
53
-
54
- Add the baseline page styles to application CSS:
55
-
56
- ```css
57
- body.plugin {
58
- padding: 0 16px;
59
- background-color: var(--ring-content-background-color);
60
- color: var(--ring-text-color);
61
- font-size: var(--ring-font-size);
62
- }
63
- ```
64
-
65
- A deliberately full-bleed shell can override the body padding in application CSS.
66
-
67
- ## Complete page shell
68
-
69
- `page-shell.tsx`:
70
-
71
- ```tsx
72
- import type {ReactNode} from 'react';
73
- import Button from '@jetbrains/ring-ui-built/components/button/button';
74
- import {H1} from '@jetbrains/ring-ui-built/components/heading/heading';
75
-
76
- import styles from './page-shell.module.css';
77
-
78
- type PageShellProps = {
79
- children: ReactNode;
80
- currentPath: string;
81
- onCreateProject: () => void;
82
- };
83
-
84
- const primaryNavigation = [
85
- {href: '/projects', label: 'Projects'},
86
- {href: '/teams', label: 'Teams'},
87
- ];
88
-
89
- const settingsNavigation = [
90
- {href: '/projects/settings/general', label: 'General'},
91
- {href: '/projects/settings/access', label: 'Access'},
92
- {href: '/projects/settings/integrations', label: 'Integrations'},
93
- ];
94
-
95
- export function PageShell({children, currentPath, onCreateProject}: PageShellProps) {
96
- return (
97
- <div className={styles.shell}>
98
- <a className={styles.skipLink} href="#main-content">
99
- Skip to content
100
- </a>
101
-
102
- <header className={styles.topBar}>
103
- <a className={styles.brand} href="/" aria-label="Acme home">
104
- Acme
105
- </a>
106
- <nav className={styles.topNavigation} aria-label="Primary">
107
- {primaryNavigation.map(item => (
108
- <a
109
- key={item.href}
110
- className={styles.navigationLink}
111
- href={item.href}
112
- aria-current={currentPath === item.href ? 'page' : undefined}
113
- >
114
- {item.label}
115
- </a>
116
- ))}
117
- </nav>
118
- <Button primary onClick={onCreateProject}>
119
- New project
120
- </Button>
121
- </header>
122
-
123
- <div className={styles.workspace}>
124
- <aside className={styles.sidebar}>
125
- <nav className={styles.sidebarNavigation} aria-label="Project settings">
126
- {settingsNavigation.map(item => (
127
- <a
128
- key={item.href}
129
- className={styles.sidebarLink}
130
- href={item.href}
131
- aria-current={currentPath === item.href ? 'page' : undefined}
132
- >
133
- {item.label}
134
- </a>
135
- ))}
136
- </nav>
137
- </aside>
138
-
139
- <main id="main-content" className={styles.main} tabIndex={-1}>
140
- <div className={styles.content}>
141
- <H1>Project settings</H1>
142
- {children}
143
- </div>
144
- </main>
145
- </div>
146
- </div>
147
- );
148
- }
149
- ```
150
-
151
- `page-shell.module.css`:
152
-
153
- ```css
154
- .shell {
155
- --page-top-offset: calc(var(--ring-unit) * 8);
156
-
157
- min-height: 100dvh;
158
- color: var(--ring-text-color);
159
- background: var(--ring-content-background-color);
160
- }
161
-
162
- .skipLink {
163
- position: absolute;
164
- z-index: 20;
165
- inset-block-start: var(--ring-unit);
166
- inset-inline-start: var(--ring-unit);
167
- padding: var(--ring-unit) calc(var(--ring-unit) * 2);
168
- color: var(--ring-link-color);
169
- background: var(--ring-content-background-color);
170
- border: 1px solid var(--ring-borders-color);
171
- transform: translateY(-200%);
172
- }
173
-
174
- .skipLink:focus {
175
- transform: none;
176
- }
177
-
178
- .topBar {
179
- position: sticky;
180
- z-index: 10;
181
- inset-block-start: 0;
182
- display: flex;
183
- min-height: var(--page-top-offset);
184
- align-items: center;
185
- gap: calc(var(--ring-unit) * 2);
186
- padding-inline: calc(var(--ring-unit) * 4);
187
- background: var(--ring-navigation-background-color);
188
- border-block-end: 1px solid var(--ring-line-color);
189
- }
190
-
191
- .brand {
192
- flex: none;
193
- color: var(--ring-text-color);
194
- font-weight: bold;
195
- text-decoration: none;
196
- }
197
-
198
- .topNavigation {
199
- display: flex;
200
- flex: 1;
201
- flex-wrap: wrap;
202
- gap: var(--ring-unit) calc(var(--ring-unit) * 2);
203
- min-width: 0;
204
- }
205
-
206
- .navigationLink,
207
- .sidebarLink {
208
- color: var(--ring-link-color);
209
- }
210
-
211
- .navigationLink[aria-current='page'],
212
- .sidebarLink[aria-current='page'] {
213
- color: var(--ring-text-color);
214
- font-weight: bold;
215
- text-decoration: none;
216
- }
217
-
218
- .workspace {
219
- display: grid;
220
- grid-template-columns: calc(var(--ring-unit) * 30) minmax(0, 1fr);
221
- min-height: calc(100dvh - var(--page-top-offset));
222
- }
223
-
224
- .sidebar {
225
- position: sticky;
226
- inset-block-start: var(--page-top-offset);
227
- align-self: start;
228
- padding: calc(var(--ring-unit) * 3);
229
- background: var(--ring-sidebar-background-color);
230
- border-inline-end: 1px solid var(--ring-line-color);
231
- }
232
-
233
- .sidebarNavigation {
234
- display: flex;
235
- flex-direction: column;
236
- gap: var(--ring-unit);
237
- }
238
-
239
- .main {
240
- min-width: 0;
241
- padding: calc(var(--ring-unit) * 4);
242
- }
243
-
244
- .content {
245
- width: min(100%, calc(var(--ring-unit) * 120));
246
- margin-inline: auto;
247
- }
248
-
249
- @media (max-width: 760px) {
250
- .shell {
251
- --page-top-offset: 0px;
252
- }
253
-
254
- .topBar {
255
- position: static;
256
- flex-wrap: wrap;
257
- padding: calc(var(--ring-unit) * 2);
258
- }
259
-
260
- .topNavigation {
261
- order: 3;
262
- flex-basis: 100%;
263
- }
264
-
265
- .workspace {
266
- grid-template-columns: minmax(0, 1fr);
267
- min-height: 0;
268
- }
269
-
270
- .sidebar {
271
- position: static;
272
- min-width: 0;
273
- padding: var(--ring-unit) calc(var(--ring-unit) * 2);
274
- overflow-x: auto;
275
- border-inline-end: 0;
276
- border-block-end: 1px solid var(--ring-line-color);
277
- }
278
-
279
- .sidebarNavigation {
280
- width: max-content;
281
- flex-direction: row;
282
- gap: calc(var(--ring-unit) * 2);
283
- }
284
-
285
- .main {
286
- padding: calc(var(--ring-unit) * 2);
287
- }
288
- }
289
- ```
290
-
291
- ## Geometry rules
292
-
293
- - Use `--ring-unit` as the spacing base and semantic tokens such as `--ring-content-background-color`, `--ring-sidebar-background-color`, `--ring-text-color`, and `--ring-line-color`. Do not paste their current pixel or color values into application CSS.
294
- - In Grid/Flexbox, `min-width: auto` can stop content from shrinking and `min-height: auto` can stop vertical children from scrolling. Put `min-width: 0` or `min-height: 0` on the child that must shrink.
295
- - Assign exactly one scroll owner per axis and scope. Prefer document scrolling for a page; use a named inner owner only for a deliberate viewport-like region. The mobile sidebar above owns only its horizontal overflow. A dialog or portal is a separate scroll scope.
296
- - Bound long-form reading content rather than stretching it across the viewport. Choose a product-appropriate token-multiple maximum; allow tables, canvases, and dense dashboards to use a wider explicit region.
297
- - Use CSS media queries when only geometry changes. Branch in React when narrow layouts need different interaction, focus order, or content—not to reproduce `display`, wrapping, or column changes.
298
- - Use container queries for reusable widgets whose layout depends on their allocated panel width. Give the wrapper `container-type: inline-size` and keep viewport queries for the outer application shell.
299
-
300
- ## Sticky offsets and nested scopes
301
-
302
- Prefer a simple inherited CSS variable when sticky heights are known:
303
-
304
- ```css
305
- .page {
306
- --sticky-offset: calc(var(--ring-unit) * 8);
307
- }
308
-
309
- .localToolbar {
310
- position: sticky;
311
- inset-block-start: var(--sticky-offset);
312
- }
313
- ```
314
-
315
- Measure in JavaScript only when the preceding sticky height is genuinely dynamic; publish the measured value as a CSS variable instead of calculating every child position in React. Keep portal and dialog scroll containers isolated: their sticky offsets begin at that scope's top, not the document header.
316
-
317
- ## Theme, semantics, and accessibility
318
-
319
- - Put `ThemeProvider` near the application root and use `Theme.AUTO`; use `passToPopups` when popup content must inherit the theme. Keep token-based application CSS inside the same themed subtree.
320
- - Keep one real `h1` in `main`. Use `header`, labelled `nav`, `aside`, and `main` landmarks; do not use visual heading styles as a substitute for heading order.
321
- - Use native anchors for navigation so open-in-new-tab, copy-link, and browser history continue to work. Use buttons for actions.
322
- - Preserve visible focus, keyboard order, skip navigation, accessible names, and `aria-current="page"`. Ensure sticky content does not obscure focused targets or anchor destinations; use `scroll-margin-block-start` where needed.
323
-
324
- Avoid deprecated `Grid`/`Row`/`Col` and `ContentLayout`, hardcoded copies of token values, fixed positioning where sticky works, JavaScript viewport checks for pure layout, styling Ring internals or `data-test`, and product-specific frameworks or portal conventions.