@jetbrains/ring-ui 7.0.122 → 7.0.123-beta.0
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,6 +13,10 @@
|
|
|
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
|
+
|
|
16
20
|
## Try now
|
|
17
21
|
* 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
|
|
18
22
|
* Check out [list of examples](https://jetbrains.github.io/ring-ui/master/index.html) for each component
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetbrains/ring-ui",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.123-beta.0",
|
|
4
4
|
"description": "JetBrains UI library",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "JetBrains"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"babel.config.js",
|
|
12
12
|
"components",
|
|
13
13
|
"postcss.config.js",
|
|
14
|
+
"skills",
|
|
14
15
|
"webpack.config.js",
|
|
15
16
|
"typings.d.ts"
|
|
16
17
|
],
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"prebuild": "rimraf components && npm run prebuild:tsc && cpy './**/*' '!**/*.stories.*' '!**/*.figma.*' '!**/*.ts' '!**/*.tsx' '!**/test-helpers/mocks/**' ../components --parents --cwd=src/",
|
|
69
70
|
"prebuild:tsc": "(npm run type-check:create-d-ts && tsc --project tsconfig-build.json) ; npm run type-check:cleanup-d-ts",
|
|
70
71
|
"prepare": "webpack -c .storybook/custom-header/webpack.config.js",
|
|
71
|
-
"prepublishOnly": "pinst --disable",
|
|
72
|
+
"prepublishOnly": "npm run verify-package-skills && pinst --disable",
|
|
72
73
|
"prerelease-built-ci": "node scripts/prepare-built-package.js",
|
|
73
74
|
"prerelease-ci": "git pull",
|
|
74
75
|
"release-built-ci": "npm publish --access=public $NPM_PUBLISH_PARAMS",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"stylelint": "stylelint --ignore-path .stylelintignore '**/*.css'",
|
|
84
85
|
"stylelint-ci": "stylelint --ignore-path .stylelintignore --custom-formatter 'scripts/jslint-xml.js' '**/*.css' | xmlappend eslint-report.xml",
|
|
85
86
|
"test": "vitest src",
|
|
87
|
+
"verify-package-skills": "node scripts/verify-package-skills.mjs",
|
|
86
88
|
"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",
|
|
87
89
|
"type-check:create-d-ts": "npx tcm src && npx tcm .storybook",
|
|
88
90
|
"type-check:main": "tsc --noEmit -p tsconfig.json",
|
|
@@ -0,0 +1,43 @@
|
|
|
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.
|
|
@@ -0,0 +1,261 @@
|
|
|
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.
|
|
@@ -0,0 +1,324 @@
|
|
|
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.
|