@jetbrains/ring-ui-built 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.
Files changed (73) hide show
  1. package/README.md +0 -4
  2. package/components/_helpers/caption.js +1 -1
  3. package/components/_helpers/icon-svg.js +50 -35
  4. package/components/alert/alert.js +1 -1
  5. package/components/alert-service/alert-service.js +1 -1
  6. package/components/auth/auth-core.js +1 -1
  7. package/components/auth/auth.js +1 -1
  8. package/components/auth/down-notification.js +1 -1
  9. package/components/auth/storage.js +1 -1
  10. package/components/avatar-stack/avatar-stack.js +1 -1
  11. package/components/banner/banner.js +1 -1
  12. package/components/button/button.js +1 -1
  13. package/components/button-toolbar/button-toolbar.js +1 -1
  14. package/components/checkbox/checkbox.js +1 -0
  15. package/components/clipboard/clipboard.js +1 -1
  16. package/components/collapsible-group/collapsible-group.js +1 -0
  17. package/components/confirm-service/confirm-service.js +1 -1
  18. package/components/contenteditable/contenteditable.js +4 -3
  19. package/components/data-list/data-list.js +1 -1
  20. package/components/data-list/data-list.mock.js +1 -1
  21. package/components/data-list/item.js +1 -1
  22. package/components/data-list/title.js +1 -0
  23. package/components/date-picker/date-popup.js +1 -1
  24. package/components/dialog/dialog.js +1 -1
  25. package/components/dropdown/anchor.js +1 -1
  26. package/components/dropdown/dropdown.js +1 -1
  27. package/components/dropdown-menu/dropdown-menu.js +1 -1
  28. package/components/editable-heading/editable-heading.js +1 -1
  29. package/components/error-message/error-message.js +1 -0
  30. package/components/expand/collapsible-group.js +1 -0
  31. package/components/global/configuration.d.ts +5 -0
  32. package/components/global/configuration.js +10 -2
  33. package/components/global/controls-height.js +1 -1
  34. package/components/header/header-icon.js +1 -1
  35. package/components/header/header.js +1 -1
  36. package/components/header/logo.js +1 -0
  37. package/components/header/profile.js +1 -1
  38. package/components/header/services.js +1 -1
  39. package/components/header/smart-profile.js +1 -1
  40. package/components/header/smart-services.js +1 -1
  41. package/components/icon/icon-svg.js +1 -0
  42. package/components/icon/icon.js +1 -0
  43. package/components/icon/index.js +1 -0
  44. package/components/input/input.js +1 -1
  45. package/components/list/list-item.js +1 -0
  46. package/components/list/list-users-groups-source.js +1 -0
  47. package/components/list/list.js +1 -0
  48. package/components/login-dialog/login-dialog.js +1 -1
  49. package/components/old-browsers-message/white-list.js +2 -2
  50. package/components/query-assist/query-assist-suggestions.js +1 -0
  51. package/components/select/chevron-button.js +1 -1
  52. package/components/select/select-filter.js +1 -1
  53. package/components/storage/storage-local.js +1 -1
  54. package/components/storage/storage.js +1 -1
  55. package/components/style.css +1 -1
  56. package/components/table/header-cell.js +1 -0
  57. package/components/table/header.js +1 -0
  58. package/components/table/row-with-focus-sensor.js +1 -1
  59. package/components/table/row.js +1 -1
  60. package/components/table/simple-table.js +1 -1
  61. package/components/table/smart-table.js +1 -1
  62. package/components/table/table.js +1 -1
  63. package/components/tags-input/tags-input.js +1 -1
  64. package/components/tags-list/tags-list.js +1 -1
  65. package/components/upload/upload.js +1 -0
  66. package/components/user-agreement/service.js +1 -1
  67. package/components/user-card/smart-user-card-tooltip.js +1 -1
  68. package/components/user-card/tooltip.js +1 -1
  69. package/components/user-card/user-card.js +1 -1
  70. package/package.json +5 -7
  71. package/skills/ring-ui/SKILL.md +0 -43
  72. package/skills/ring-ui/references/forms-and-feedback.md +0 -261
  73. package/skills/ring-ui/references/page-layouts.md +0 -324
@@ -10,6 +10,7 @@ import 'util-deprecate';
10
10
  import '../icon/icon.constants.js';
11
11
  import '../_helpers/icon-svg.js';
12
12
  import 'react-compiler-runtime';
13
+ import '../global/configuration.js';
13
14
  import '../global/memoize.js';
14
15
 
15
16
  class HeaderCell extends PureComponent {
@@ -12,6 +12,7 @@ import 'util-deprecate';
12
12
  import '../icon/icon.constants.js';
13
13
  import '../_helpers/icon-svg.js';
14
14
  import 'react-compiler-runtime';
15
+ import '../global/configuration.js';
15
16
  import '../global/memoize.js';
16
17
  import '../global/compose-refs.js';
17
18
  import 'memoize-one';
@@ -16,6 +16,7 @@ import '../icon/icon.js';
16
16
  import 'util-deprecate';
17
17
  import '../icon/icon.constants.js';
18
18
  import '../_helpers/icon-svg.js';
19
+ import '../global/configuration.js';
19
20
  import '../global/memoize.js';
20
21
  import '../control-help/control-help.js';
21
22
  import '../global/data-tests.js';
@@ -23,7 +24,6 @@ import '../button/button.js';
23
24
  import '@jetbrains/icons/chevron-12px-down';
24
25
  import '../link/clickable-link.js';
25
26
  import '../global/controls-height.js';
26
- import '../global/configuration.js';
27
27
  import '../_helpers/button.classes.js';
28
28
  import '../tooltip/tooltip.js';
29
29
  import '../popup/popup.js';
@@ -19,12 +19,12 @@ import '../icon/icon.js';
19
19
  import 'util-deprecate';
20
20
  import '../icon/icon.constants.js';
21
21
  import '../_helpers/icon-svg.js';
22
+ import '../global/configuration.js';
22
23
  import '../global/memoize.js';
23
24
  import '../control-help/control-help.js';
24
25
  import '@jetbrains/icons/chevron-12px-down';
25
26
  import '../link/clickable-link.js';
26
27
  import '../global/controls-height.js';
27
- import '../global/configuration.js';
28
28
  import '../_helpers/button.classes.js';
29
29
  import '../popup/popup.js';
30
30
  import 'react-dom';
@@ -27,6 +27,7 @@ import 'util-deprecate';
27
27
  import '../icon/icon.constants.js';
28
28
  import '../_helpers/icon-svg.js';
29
29
  import 'react-compiler-runtime';
30
+ import '../global/configuration.js';
30
31
  import '../global/memoize.js';
31
32
  import '../control-help/control-help.js';
32
33
  import '../global/data-tests.js';
@@ -43,7 +44,6 @@ import '@jetbrains/icons/drag';
43
44
  import '../button/button.js';
44
45
  import '../link/clickable-link.js';
45
46
  import '../global/controls-height.js';
46
- import '../global/configuration.js';
47
47
  import '../_helpers/button.classes.js';
48
48
  import '../tooltip/tooltip.js';
49
49
  import '../popup/popup.js';
@@ -26,6 +26,7 @@ import 'util-deprecate';
26
26
  import '../icon/icon.constants.js';
27
27
  import '../_helpers/icon-svg.js';
28
28
  import 'react-compiler-runtime';
29
+ import '../global/configuration.js';
29
30
  import '../global/memoize.js';
30
31
  import '../control-help/control-help.js';
31
32
  import '../global/data-tests.js';
@@ -43,7 +44,6 @@ import '@jetbrains/icons/drag';
43
44
  import '../button/button.js';
44
45
  import '../link/clickable-link.js';
45
46
  import '../global/controls-height.js';
46
- import '../global/configuration.js';
47
47
  import '../_helpers/button.classes.js';
48
48
  import '../tooltip/tooltip.js';
49
49
  import '../popup/popup.js';
@@ -28,6 +28,7 @@ import 'util-deprecate';
28
28
  import '../icon/icon.constants.js';
29
29
  import '../_helpers/icon-svg.js';
30
30
  import 'react-compiler-runtime';
31
+ import '../global/configuration.js';
31
32
  import '../global/memoize.js';
32
33
  import '../control-help/control-help.js';
33
34
  import '../global/data-tests.js';
@@ -41,7 +42,6 @@ import '@jetbrains/icons/drag';
41
42
  import '../button/button.js';
42
43
  import '../link/clickable-link.js';
43
44
  import '../global/controls-height.js';
44
- import '../global/configuration.js';
45
45
  import '../_helpers/button.classes.js';
46
46
  import '../tooltip/tooltip.js';
47
47
  import '../popup/popup.js';
@@ -25,9 +25,9 @@ import 'util-deprecate';
25
25
  import '../icon/icon.js';
26
26
  import '../icon/icon.constants.js';
27
27
  import '../_helpers/icon-svg.js';
28
+ import '../global/configuration.js';
28
29
  import '../link/clickable-link.js';
29
30
  import '../_helpers/button.classes.js';
30
- import '../global/configuration.js';
31
31
  import '../avatar/avatar.js';
32
32
  import 'core-js/modules/es.array.includes.js';
33
33
  import '../global/url.js';
@@ -8,13 +8,13 @@ import 'util-deprecate';
8
8
  import '../icon/icon.constants.js';
9
9
  import '../_helpers/icon-svg.js';
10
10
  import 'react-compiler-runtime';
11
+ import '../global/configuration.js';
11
12
  import '../global/memoize.js';
12
13
  import '../button/button.js';
13
14
  import '@jetbrains/icons/chevron-down';
14
15
  import '@jetbrains/icons/chevron-12px-down';
15
16
  import '../link/clickable-link.js';
16
17
  import '../global/controls-height.js';
17
- import '../global/configuration.js';
18
18
  import '../_helpers/button.classes.js';
19
19
  import '../global/data-tests.js';
20
20
 
@@ -7,6 +7,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
7
7
  import 'util-deprecate';
8
8
  import '../icon/icon.constants.js';
9
9
  import '../_helpers/icon-svg.js';
10
+ import '../global/configuration.js';
10
11
  import '../global/memoize.js';
11
12
 
12
13
  var styles = {"upload":"ring-upload-upload","success":"ring-upload-success","error":"ring-upload-error","dragOver":"ring-upload-dragOver","disabled":"ring-upload-disabled","invisibleFileInput":"ring-upload-invisibleFileInput","attachmentIcon":"ring-upload-attachmentIcon"};
@@ -20,6 +20,7 @@ import 'util-deprecate';
20
20
  import '../icon/icon.constants.js';
21
21
  import '../_helpers/icon-svg.js';
22
22
  import 'react-compiler-runtime';
23
+ import '../global/configuration.js';
23
24
  import '../global/memoize.js';
24
25
  import '../loader-inline/loader-inline.js';
25
26
  import '../global/data-tests.js';
@@ -29,7 +30,6 @@ import '@jetbrains/icons/chevron-down';
29
30
  import '@jetbrains/icons/chevron-12px-down';
30
31
  import '../link/clickable-link.js';
31
32
  import '../_helpers/button.classes.js';
32
- import '../global/configuration.js';
33
33
  import '../_helpers/theme.js';
34
34
  import '../alert/alert-heading.js';
35
35
  import '../heading/heading.js';
@@ -16,10 +16,10 @@ import 'util-deprecate';
16
16
  import '../icon/icon.js';
17
17
  import '../icon/icon.constants.js';
18
18
  import '../_helpers/icon-svg.js';
19
+ import '../global/configuration.js';
19
20
  import '../global/memoize.js';
20
21
  import '../link/clickable-link.js';
21
22
  import '../global/controls-height.js';
22
- import '../global/configuration.js';
23
23
  import '../_helpers/button.classes.js';
24
24
  import '../popup/popup.js';
25
25
  import 'react-dom';
@@ -15,10 +15,10 @@ import 'util-deprecate';
15
15
  import '../icon/icon.js';
16
16
  import '../icon/icon.constants.js';
17
17
  import '../_helpers/icon-svg.js';
18
+ import '../global/configuration.js';
18
19
  import '../global/memoize.js';
19
20
  import '../link/clickable-link.js';
20
21
  import '../global/controls-height.js';
21
- import '../global/configuration.js';
22
22
  import '../_helpers/button.classes.js';
23
23
  import 'react-dom';
24
24
  import '../global/get-uid.js';
@@ -32,12 +32,12 @@ import '@jetbrains/icons/close';
32
32
  import '../icon/icon.js';
33
33
  import '../icon/icon.constants.js';
34
34
  import '../_helpers/icon-svg.js';
35
+ import '../global/configuration.js';
35
36
  import '../loader-inline/loader-inline.js';
36
37
  import '../button/button.js';
37
38
  import '@jetbrains/icons/chevron-down';
38
39
  import '@jetbrains/icons/chevron-12px-down';
39
40
  import '../global/controls-height.js';
40
- import '../global/configuration.js';
41
41
  import '../_helpers/button.classes.js';
42
42
  import '../_helpers/theme.js';
43
43
  import '../alert/alert-heading.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetbrains/ring-ui-built",
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"
@@ -9,7 +9,6 @@
9
9
  "packageManager": "npm@8.11.0",
10
10
  "files": [
11
11
  "components",
12
- "skills",
13
12
  "typings.d.ts"
14
13
  ],
15
14
  "exports": {
@@ -57,7 +56,7 @@
57
56
  "prelint-ci": "echo \"##teamcity[importData type='jslint' path='eslint-report.xml']\"",
58
57
  "lint-ci": "eslint --format jslint-xml . > eslint-report.xml && npm run stylelint-ci",
59
58
  "lint:js": "eslint",
60
- "postbuild": "cpy './**/*.d.ts' ../dist --parents --cwd=components/",
59
+ "postbuild": "cpy './**/*.d.ts' ../dist --parents --cwd=components/ && node scripts/check-built-css.mjs",
61
60
  "_postinstall": "husky && npm run postinstall:gitconfig",
62
61
  "postinstall:gitconfig": "git config blame.ignoreRevsFile .git-blame-ignore-revs",
63
62
  "postpublish": "pinst --enable",
@@ -66,7 +65,7 @@
66
65
  "prebuild": "rimraf components && npm run prebuild:tsc && cpy './**/*' '!**/*.stories.*' '!**/*.figma.*' '!**/*.ts' '!**/*.tsx' '!**/test-helpers/mocks/**' ../components --parents --cwd=src/",
67
66
  "prebuild:tsc": "(npm run type-check:create-d-ts && tsc --project tsconfig-build.json) ; npm run type-check:cleanup-d-ts",
68
67
  "prepare": "webpack -c .storybook/custom-header/webpack.config.js",
69
- "prepublishOnly": "npm run verify-package-skills && pinst --disable",
68
+ "prepublishOnly": "pinst --disable",
70
69
  "prerelease-built-ci": "node scripts/prepare-built-package.js",
71
70
  "prerelease-ci": "git pull",
72
71
  "release-built-ci": "npm publish --access=public $NPM_PUBLISH_PARAMS",
@@ -80,8 +79,7 @@
80
79
  "storybook-debug": "node --inspect-brk node_modules/@storybook/react/bin -p 9999",
81
80
  "stylelint": "stylelint --ignore-path .stylelintignore '**/*.css'",
82
81
  "stylelint-ci": "stylelint --ignore-path .stylelintignore --custom-formatter 'scripts/jslint-xml.js' '**/*.css' | xmlappend eslint-report.xml",
83
- "test": "vitest src",
84
- "verify-package-skills": "node scripts/verify-package-skills.mjs",
82
+ "test": "vitest src && npm test --workspace @jetbrains/rollup-css-plugin",
85
83
  "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",
86
84
  "type-check:create-d-ts": "npx tcm src && npx tcm .storybook",
87
85
  "type-check:main": "tsc --noEmit -p tsconfig.json",
@@ -115,7 +113,7 @@
115
113
  },
116
114
  "dependencies": {
117
115
  "@jetbrains/icons": "^5.23.0",
118
- "change-case": "^4.1.1",
116
+ "change-case": "^5.4.4",
119
117
  "classnames": "^2.5.1",
120
118
  "combokeys": "^3.0.1",
121
119
  "date-fns": "^4.4.0",
@@ -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.