@wallarm-org/design-system 1.32.0 → 1.33.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.
@@ -23,7 +23,7 @@ export interface CodeEditorRootProps extends CodeEditorRootNativeProps, Testable
23
23
  * document would be seeded with the previous document's text.
24
24
  */
25
25
  documentId?: string;
26
- /** Focusable, selectable, searchable, copyable — but not editable by the user */
26
+ /** Non-editable and unfocusable content; text remains selectable, searchable and copyable */
27
27
  readOnly?: boolean;
28
28
  /** Offsets gutter numbers; `lines`, `folds`, `diagnostics` use absolute numbers */
29
29
  startingLineNumber?: number;
@@ -41,9 +41,20 @@ const SearchPanel = ({ view, query, matchCount, readOnly, testId })=>{
41
41
  ]
42
42
  });
43
43
  };
44
+ const blurReadOnlyContent = ()=>{
45
+ if (view.state.readOnly && view.hasFocus) view.contentDOM.blur();
46
+ };
47
+ const closeSearch = ()=>{
48
+ closeSearchPanel(view);
49
+ blurReadOnlyContent();
50
+ };
44
51
  const handleKeyDown = (event, onEnter)=>{
45
52
  if (event.nativeEvent.isComposing || 229 === event.keyCode) return;
46
- if (runScopeHandlers(view, event.nativeEvent, 'search-panel')) return void event.preventDefault();
53
+ if (runScopeHandlers(view, event.nativeEvent, 'search-panel')) {
54
+ event.preventDefault();
55
+ blurReadOnlyContent();
56
+ return;
57
+ }
47
58
  if ('Enter' === event.key) {
48
59
  event.preventDefault();
49
60
  onEnter(event.shiftKey);
@@ -53,7 +64,10 @@ const SearchPanel = ({ view, query, matchCount, readOnly, testId })=>{
53
64
  const handleReplaceKeyDown = (event)=>handleKeyDown(event, ()=>replaceNext(view));
54
65
  const handlePanelKeyDown = (event)=>{
55
66
  if (event.target instanceof HTMLInputElement) return;
56
- if (runScopeHandlers(view, event.nativeEvent, 'search-panel')) event.preventDefault();
67
+ if (runScopeHandlers(view, event.nativeEvent, 'search-panel')) {
68
+ event.preventDefault();
69
+ blurReadOnlyContent();
70
+ }
57
71
  };
58
72
  const canSearch = query.valid;
59
73
  return /*#__PURE__*/ jsxs("div", {
@@ -154,7 +168,7 @@ const SearchPanel = ({ view, query, matchCount, readOnly, testId })=>{
154
168
  className: "ml-auto",
155
169
  "aria-label": "Close search",
156
170
  "data-testid": slot('search-close'),
157
- onClick: ()=>closeSearchPanel(view),
171
+ onClick: closeSearch,
158
172
  children: /*#__PURE__*/ jsx(X, {})
159
173
  })
160
174
  ]
@@ -4,9 +4,9 @@ import type { CodeEditorCompletionContext, CodeEditorCompletionSource, CodeEdito
4
4
  /** Public, engine-free view of a CodeMirror completion request (spec §4). */
5
5
  export declare const buildCompletionContext: (ctx: CompletionContext, language: CodeEditorLanguage, startingLineNumber: number) => CodeEditorCompletionContext;
6
6
  /**
7
- * Autocomplete (spec §7.14): built-in HTTP source for `http`, the JSON Schema source when
8
- * `schema` is set, then consumer sources, all through `override` (language-data sources are
9
- * off). `completionKeymap` comes with `defaultKeymap: true` at `Prec.highest`, so Enter,
7
+ * Autocomplete: HTTP and JSON Schema sources, consumer sources, then language keywords and
8
+ * document words, all through `override` (language-data sources are off).
9
+ * `completionKeymap` comes with `defaultKeymap: true` at `Prec.highest`, so Enter,
10
10
  * arrows and Escape reach the open list before `indentWithTab` / fullscreen Escape.
11
11
  */
12
12
  export declare const completionExtension: (config: {
@@ -1,4 +1,4 @@
1
- import { autocompletion } from "@codemirror/autocomplete";
1
+ import { autocompletion, completeAnyWord, completeFromList } from "@codemirror/autocomplete";
2
2
  import { EditorView } from "@codemirror/view";
3
3
  import { cn } from "../../../utils/cn.js";
4
4
  import { dropdownMenuItemVariants } from "../../DropdownMenu/classes.js";
@@ -106,13 +106,153 @@ const adaptSource = (source, language, startingLineNumber)=>(ctx)=>{
106
106
  }
107
107
  };
108
108
  const reopensAfter = (completion)=>'string' == typeof completion.apply && completion.apply.endsWith(': ');
109
+ const LANGUAGE_KEYWORDS = {
110
+ json: [
111
+ 'true',
112
+ 'false',
113
+ 'null'
114
+ ],
115
+ yaml: [
116
+ 'true',
117
+ 'false',
118
+ 'null'
119
+ ],
120
+ bash: [
121
+ 'echo',
122
+ 'export',
123
+ 'if',
124
+ 'then',
125
+ 'else',
126
+ 'fi',
127
+ 'for',
128
+ 'in',
129
+ 'do',
130
+ 'done',
131
+ 'function'
132
+ ],
133
+ javascript: [
134
+ 'const',
135
+ 'let',
136
+ 'var',
137
+ 'function',
138
+ 'return',
139
+ 'if',
140
+ 'else',
141
+ 'for',
142
+ 'while',
143
+ 'class',
144
+ 'import',
145
+ 'export',
146
+ 'from',
147
+ 'async',
148
+ 'await',
149
+ 'true',
150
+ 'false',
151
+ 'null',
152
+ 'undefined'
153
+ ],
154
+ typescript: [
155
+ 'const',
156
+ 'let',
157
+ 'function',
158
+ 'return',
159
+ 'if',
160
+ 'else',
161
+ 'class',
162
+ 'import',
163
+ 'export',
164
+ 'from',
165
+ 'async',
166
+ 'await',
167
+ 'interface',
168
+ 'type',
169
+ 'enum',
170
+ 'extends',
171
+ 'implements',
172
+ 'public',
173
+ 'private',
174
+ 'readonly',
175
+ 'true',
176
+ 'false',
177
+ 'null',
178
+ 'undefined'
179
+ ],
180
+ python: [
181
+ 'def',
182
+ 'class',
183
+ 'return',
184
+ 'if',
185
+ 'elif',
186
+ 'else',
187
+ 'for',
188
+ 'while',
189
+ 'in',
190
+ 'import',
191
+ 'from',
192
+ 'as',
193
+ 'async',
194
+ 'await',
195
+ 'try',
196
+ 'except',
197
+ 'with',
198
+ 'True',
199
+ 'False',
200
+ 'None'
201
+ ],
202
+ lua: [
203
+ 'function',
204
+ 'local',
205
+ 'return',
206
+ 'if',
207
+ 'then',
208
+ 'elseif',
209
+ 'else',
210
+ 'end',
211
+ 'for',
212
+ 'while',
213
+ 'do',
214
+ 'repeat',
215
+ 'until',
216
+ 'true',
217
+ 'false',
218
+ 'nil'
219
+ ]
220
+ };
221
+ const isInsideQuotedString = (text)=>{
222
+ let quoted = false;
223
+ for(let i = 0; i < text.length; i++)if ('\\' === text[i]) i++;
224
+ else if ('"' === text[i]) quoted = !quoted;
225
+ return quoted;
226
+ };
227
+ const builtInSources = (language)=>{
228
+ const keywords = LANGUAGE_KEYWORDS[language];
229
+ if (!keywords) return [
230
+ completeAnyWord
231
+ ];
232
+ const completeKeywords = completeFromList(keywords.map((label)=>({
233
+ label,
234
+ type: 'keyword'
235
+ })));
236
+ if ('json' === language) return [
237
+ (ctx)=>{
238
+ const line = ctx.state.doc.lineAt(ctx.pos);
239
+ const before = line.text.slice(0, ctx.pos - line.from);
240
+ if (isInsideQuotedString(before)) return completeAnyWord(ctx);
241
+ return /(?:^|[:,\[])[\s\w]*$/.test(before) ? completeKeywords(ctx) : null;
242
+ }
243
+ ];
244
+ return [
245
+ completeKeywords,
246
+ completeAnyWord
247
+ ];
248
+ };
109
249
  const completionExtension = (config)=>{
110
250
  const { language, schema, sources, startingLineNumber } = config;
111
251
  const override = [];
112
252
  if ('http' === language) override.push(adaptSource(httpCompletions, language, startingLineNumber));
113
253
  if (void 0 !== schema && isJsonLike(language)) override.push(schemaCompletionSource(()=>schema, (state)=>jsonRegion(state, language)));
114
254
  for (const source of sources)override.push(adaptSource(source, language, startingLineNumber));
115
- if (0 === override.length) return [];
255
+ override.push(...builtInSources(language));
116
256
  return [
117
257
  autocompletion({
118
258
  override,
@@ -79,7 +79,8 @@ const SLOT_DEPS = {
79
79
  ],
80
80
  contentAttributes: [
81
81
  'contentAttributes',
82
- 'testId'
82
+ 'testId',
83
+ 'readOnly'
83
84
  ],
84
85
  cspNonce: [
85
86
  'cspNonce'
@@ -184,6 +185,7 @@ const featureExtensions = (options, callbacks)=>({
184
185
  });
185
186
  const contentAttributesFor = (options)=>{
186
187
  const attrs = sanitizeContentAttributes(options.contentAttributes);
188
+ if (options.readOnly) delete attrs.tabindex;
187
189
  if (options.testId) attrs['data-testid'] = `${options.testId}--editor`;
188
190
  return attrs;
189
191
  };
@@ -225,7 +227,10 @@ const gutterAria = [
225
227
  ];
226
228
  const slotExtensions = (options, callbacks)=>({
227
229
  language: ()=>languageExtension(options.language),
228
- readOnly: ()=>EditorState.readOnly.of(options.readOnly),
230
+ readOnly: ()=>[
231
+ EditorState.readOnly.of(options.readOnly),
232
+ EditorView.editable.of(!options.readOnly)
233
+ ],
229
234
  wrap: ()=>options.wrapLines ? EditorView.lineWrapping : [],
230
235
  maxHeight: ()=>maxHeightTheme(options.maxHeight),
231
236
  contentAttributes: ()=>EditorView.contentAttributes.of(contentAttributesFor(options)),
@@ -298,6 +303,10 @@ const createEditor = (parent, initialOptions, callbacks)=>{
298
303
  state: createState(initialOptions.value, initialOptions),
299
304
  parent
300
305
  });
306
+ const preventReadOnlyMouseFocus = (event)=>{
307
+ if (view.state.readOnly && event.target instanceof Node && view.contentDOM.contains(event.target)) event.stopPropagation();
308
+ };
309
+ view.dom.addEventListener('mousedown', preventReadOnlyMouseFocus, true);
301
310
  const ensureLazyLanguage = (lang)=>{
302
311
  if (!isLazyLanguage(lang)) return;
303
312
  loadLanguageExtension(lang).then((extension)=>{
@@ -346,13 +355,20 @@ const createEditor = (parent, initialOptions, callbacks)=>{
346
355
  const update = (next)=>{
347
356
  const prev = options;
348
357
  options = next;
349
- if (next.documentId !== prev.documentId) return void swapDocument(prev, next);
358
+ if (next.documentId !== prev.documentId) {
359
+ swapDocument(prev, next);
360
+ if (next.readOnly && view.hasFocus) view.contentDOM.blur();
361
+ return;
362
+ }
350
363
  reconfigure(prev, next);
364
+ if (next.readOnly && view.hasFocus) view.contentDOM.blur();
351
365
  if (next.language !== prev.language) ensureLazyLanguage(next.language);
352
366
  syncValue(next.value);
353
367
  };
354
368
  const api = {
355
- focus: ()=>view.focus(),
369
+ focus: ()=>{
370
+ if (!view.state.readOnly) view.focus();
371
+ },
356
372
  getValue: ()=>view.state.doc.toString(),
357
373
  insertText: (text)=>{
358
374
  if (view.state.readOnly) return;
@@ -380,6 +396,7 @@ const createEditor = (parent, initialOptions, callbacks)=>{
380
396
  destroy: ()=>{
381
397
  destroyed = true;
382
398
  cache.clear();
399
+ view.dom.removeEventListener('mousedown', preventReadOnlyMouseFocus, true);
383
400
  view.destroy();
384
401
  }
385
402
  };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.31.0",
3
- "generatedAt": "2026-10-01T11:56:51.596Z",
2
+ "version": "1.32.0",
3
+ "generatedAt": "2026-10-02T12:06:48.075Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -19208,7 +19208,7 @@
19208
19208
  "name": "readOnly",
19209
19209
  "type": "boolean | undefined",
19210
19210
  "required": false,
19211
- "description": "Focusable, selectable, searchable, copyable — but not editable by the user"
19211
+ "description": "Non-editable and unfocusable content; text remains selectable, searchable and copyable"
19212
19212
  },
19213
19213
  {
19214
19214
  "name": "startingLineNumber",
@@ -19882,7 +19882,7 @@
19882
19882
  "name": "readOnly",
19883
19883
  "type": "boolean | undefined",
19884
19884
  "required": false,
19885
- "description": "Focusable, selectable, searchable, copyable — but not editable by the user",
19885
+ "description": "Non-editable and unfocusable content; text remains selectable, searchable and copyable",
19886
19886
  "defaultValue": "false"
19887
19887
  },
19888
19888
  {
@@ -20301,7 +20301,7 @@
20301
20301
  {
20302
20302
  "name": "ReadOnly",
20303
20303
  "code": "() => (\n <div style={{ width: '600px' }}>\n <CodeEditorRoot\n value={httpResponseCode}\n language='http'\n readOnly\n data-testid='code-editor-read-only'\n >\n <CodeSnippetHeader>\n <CodeSnippetTitle>Response</CodeSnippetTitle>\n <CodeSnippetActions>\n <CodeSnippetCopyButton />\n </CodeSnippetActions>\n </CodeSnippetHeader>\n <CodeEditorContent lineNumbers aria-label='HTTP response, read only' />\n </CodeEditorRoot>\n </div>\n)",
20304
- "description": "`readOnly` still lets you focus, select, search and copy, but not type. Use it when the value\ncan change elsewhere but not here. If it never changes, use `CodeSnippet`."
20304
+ "description": "`readOnly` lets you select, search and copy without focusing the code or showing a caret. Use it when the value\ncan change elsewhere but not here. If it never changes, use `CodeSnippet`."
20305
20305
  },
20306
20306
  {
20307
20307
  "name": "Tabs",
@@ -20344,9 +20344,9 @@
20344
20344
  "description": "Parity check for the three sizes: the same font size, line height and gutter padding per size."
20345
20345
  },
20346
20346
  {
20347
- "name": "ParityChrome",
20348
- "code": "() => (\n <VStack gap={24}>\n <ParityPair testId='parity-chrome-header' code={sampleCode} language='text'>\n {PARITY_HEADER_CHROME}\n </ParityPair>\n <ParityPair testId='parity-chrome-floating' code={sampleCode} language='text'>\n {PARITY_FLOATING_CHROME}\n </ParityPair>\n </VStack>\n)",
20349
- "description": "Parity check for the shared chrome: a header with tabs and actions, and floating actions\nwithout a header, sit in the same place with the same spacing."
20347
+ "name": "ParityFrame",
20348
+ "code": "() => (\n <VStack gap={24}>\n <ParityPair testId='parity-frame-header' code={sampleCode} language='text'>\n {PARITY_HEADER_FRAME}\n </ParityPair>\n <ParityPair testId='parity-frame-floating' code={sampleCode} language='text'>\n {PARITY_FLOATING_FRAME}\n </ParityPair>\n </VStack>\n)",
20349
+ "description": "Parity check for the shared frame: a header with tabs and actions, and floating actions\nwithout a header, sit in the same place with the same spacing."
20350
20350
  },
20351
20351
  {
20352
20352
  "name": "ParityShowMore",
@@ -20391,7 +20391,7 @@
20391
20391
  {
20392
20392
  "name": "LongDocument",
20393
20393
  "code": "() => <LongDocumentDemo />",
20394
- "description": "2 000 read-only lines clamped by `maxLines` — the top of the envelope the colour painter is\nbuilt for, where `readOnly` still lets the text be focused, searched and copied."
20394
+ "description": "2 000 read-only lines clamped by `maxLines` — the top of the envelope the colour painter is\nbuilt for, where `readOnly` still lets the text be selected, searched and copied."
20395
20395
  }
20396
20396
  ]
20397
20397
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",