@api-client/ui 0.5.15 → 0.5.17

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 (34) hide show
  1. package/.github/instructions/lit-best-practices.instructions.md +1 -0
  2. package/build/src/elements/code-editor/code-editor.d.ts +1 -1
  3. package/build/src/elements/code-editor/code-editor.d.ts.map +1 -1
  4. package/build/src/elements/code-editor/code-editor.js.map +1 -1
  5. package/build/src/elements/code-editor/internals/{PlaceholderWidget.d.ts → ChipWidget.d.ts} +1 -1
  6. package/build/src/elements/code-editor/internals/ChipWidget.d.ts.map +1 -0
  7. package/build/src/elements/code-editor/internals/{PlaceholderWidget.js → ChipWidget.js} +2 -2
  8. package/build/src/elements/code-editor/internals/ChipWidget.js.map +1 -0
  9. package/build/src/elements/code-editor/internals/CodeEditor.d.ts +11 -1
  10. package/build/src/elements/code-editor/internals/CodeEditor.d.ts.map +1 -1
  11. package/build/src/elements/code-editor/internals/CodeEditor.js +58 -8
  12. package/build/src/elements/code-editor/internals/CodeEditor.js.map +1 -1
  13. package/build/src/elements/code-editor/internals/CodeEditor.styles.d.ts.map +1 -1
  14. package/build/src/elements/code-editor/internals/CodeEditor.styles.js +8 -2
  15. package/build/src/elements/code-editor/internals/CodeEditor.styles.js.map +1 -1
  16. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.d.ts.map +1 -1
  17. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js +4 -4
  18. package/build/src/elements/code-editor/internals/SuggestionMatchDecorator.js.map +1 -1
  19. package/build/src/elements/code-editor/internals/types.d.ts +4 -0
  20. package/build/src/elements/code-editor/internals/types.d.ts.map +1 -1
  21. package/build/src/elements/code-editor/internals/types.js.map +1 -1
  22. package/demo/elements/code-editor/CodeEditorDemo.ts +53 -83
  23. package/package.json +1 -1
  24. package/src/elements/code-editor/code-editor.ts +6 -1
  25. package/src/elements/code-editor/internals/{PlaceholderWidget.ts → ChipWidget.ts} +1 -1
  26. package/src/elements/code-editor/internals/CodeEditor.styles.ts +8 -2
  27. package/src/elements/code-editor/internals/CodeEditor.ts +54 -8
  28. package/src/elements/code-editor/internals/SuggestionMatchDecorator.ts +4 -5
  29. package/src/elements/code-editor/internals/types.ts +5 -0
  30. package/test/elements/autocomplete/autocomplete-input.spec.ts +71 -70
  31. package/test/elements/code-editor/code-editor.accessibility.test.ts +325 -0
  32. package/test/elements/code-editor/code-editor.test.ts +576 -0
  33. package/build/src/elements/code-editor/internals/PlaceholderWidget.d.ts.map +0 -1
  34. package/build/src/elements/code-editor/internals/PlaceholderWidget.js.map +0 -1
@@ -8,6 +8,10 @@ import type {
8
8
  SuggestionInsertEvent,
9
9
  } from '../../../src/elements/code-editor/code-editor.js'
10
10
  import '../../../src/elements/code-editor/code-editor.js'
11
+ import { stringLibrarySchema } from '@pawel-up/jexl/schemas/string.schema.js'
12
+ import { arrayLibrarySchema } from '@pawel-up/jexl/schemas/array.schema.js'
13
+ import { dateLibrarySchema } from '@pawel-up/jexl/schemas/date.schema.js'
14
+ import { mathLibrarySchema } from '@pawel-up/jexl/schemas/math.schema.js'
11
15
 
12
16
  class CodeEditorDemo extends DemoPage {
13
17
  override accessor componentName = 'Code Editor'
@@ -18,75 +22,10 @@ class CodeEditorDemo extends DemoPage {
18
22
  @reactive()
19
23
  private accessor editorValue = [
20
24
  '// Start typing function names or {{ mentions }}',
21
- 'console.log("Hello, world!")',
22
25
  '',
23
- '// Try: getData, processData, or @John, @Jane',
24
- '// Example: Ask {{John}} about the API design',
25
- '// Example: {{Jane}} can review the implementation',
26
- '',
27
- '"Hello {{John}}! Can you help with the " + getData() + " function?"',
26
+ '"Hello {{John}}! Can you help with the " + upper() + " function?"',
28
27
  ].join('\n')
29
28
 
30
- private functionSchemas: FunctionSchema[] = [
31
- {
32
- name: 'getData',
33
- description: 'Fetches data from the server',
34
- parameters: [
35
- {
36
- name: 'url',
37
- schema: { type: 'string', description: 'The URL to fetch data from' },
38
- required: true,
39
- },
40
- {
41
- name: 'options',
42
- schema: { type: 'object', description: 'Fetch options' },
43
- required: false,
44
- },
45
- ],
46
- returns: {
47
- type: 'object',
48
- description: 'The fetched data as an object',
49
- },
50
- category: 'network',
51
- },
52
- {
53
- name: 'processData',
54
- description: 'Processes raw data into a usable format',
55
- parameters: [
56
- {
57
- name: 'data',
58
- schema: { description: 'Raw data to process' },
59
- required: true,
60
- },
61
- {
62
- name: 'transformer',
63
- schema: { description: 'Optional transformer function' },
64
- required: false,
65
- },
66
- ],
67
- returns: {},
68
- category: 'data',
69
- },
70
- {
71
- category: 'validation',
72
- name: 'validateInput',
73
- description: 'Validates user input against a schema',
74
- parameters: [
75
- {
76
- name: 'input',
77
- schema: { type: 'object', description: 'The input to validate' },
78
- required: true,
79
- },
80
- {
81
- name: 'schema',
82
- schema: { type: 'object', description: 'Validation schema' },
83
- required: true,
84
- },
85
- ],
86
- returns: { type: 'object', description: 'Validation result' },
87
- },
88
- ]
89
-
90
29
  private suggestions: Suggestion[] = [
91
30
  {
92
31
  id: 'user-john',
@@ -111,6 +50,42 @@ class CodeEditorDemo extends DemoPage {
111
50
  },
112
51
  ]
113
52
 
53
+ functions: FunctionSchema[] = []
54
+
55
+ constructor() {
56
+ super()
57
+
58
+ // Initialize function schemas
59
+ this.functions = this.createFunctionSchema()
60
+ }
61
+
62
+ /**
63
+ * Creates a schema for the code-editor functions.
64
+ * This method is responsible for defining the functions that can be used in the formula,
65
+ * given the current data type.
66
+ */
67
+ createFunctionSchema(): FunctionSchema[] {
68
+ const result: FunctionSchema[] = []
69
+
70
+ for (const func of Object.values(stringLibrarySchema.functions)) {
71
+ result.push(func)
72
+ }
73
+
74
+ for (const func of Object.values(arrayLibrarySchema.functions)) {
75
+ result.push(func)
76
+ }
77
+
78
+ for (const func of Object.values(dateLibrarySchema.functions)) {
79
+ result.push(func)
80
+ }
81
+
82
+ for (const func of Object.values(mathLibrarySchema.functions)) {
83
+ result.push(func)
84
+ }
85
+
86
+ return result
87
+ }
88
+
114
89
  private handleFunctionInsert(event: CustomEvent<FunctionInsertEvent>): void {
115
90
  const { functionSchema, position } = event.detail
116
91
  this.output = `Function inserted: ${functionSchema.name} at position ${position}`
@@ -124,7 +99,12 @@ class CodeEditorDemo extends DemoPage {
124
99
  private handleInput(event: Event): void {
125
100
  const target = event.target as HTMLElement & { value: string }
126
101
  this.editorValue = target.value
127
- console.log('Editor input:', new Error().stack)
102
+ }
103
+
104
+ private handleFunctionDocumentation(event: CustomEvent<{ functionSchema: FunctionSchema }>): void {
105
+ const { functionSchema } = event.detail
106
+ this.output = `Function documentation requested for: ${functionSchema.name}`
107
+ console.log('Function documentation:', functionSchema)
128
108
  }
129
109
 
130
110
  private handleChange(event: Event): void {
@@ -145,12 +125,14 @@ class CodeEditorDemo extends DemoPage {
145
125
  label="Code Editor"
146
126
  supporting-text="Type function names or {{fnName}} to see autocomplete"
147
127
  .value=${this.editorValue}
148
- .functionSchemas=${this.functionSchemas}
128
+ .functionSchemas=${this.functions}
149
129
  .suggestions=${this.suggestions}
150
130
  @function-insert=${this.handleFunctionInsert}
151
131
  @suggestion-insert=${this.handleSuggestionInsert}
152
132
  @input=${this.handleInput}
153
133
  @change=${this.handleChange}
134
+ show-documentation
135
+ @function-documentation="${this.handleFunctionDocumentation}"
154
136
  ></code-editor>
155
137
 
156
138
  ${this.output ? html`<p><strong>Output:</strong> ${this.output}</p>` : ''}
@@ -164,7 +146,7 @@ class CodeEditorDemo extends DemoPage {
164
146
  label="Dark Theme Editor"
165
147
  supporting-text="Dark theme variant"
166
148
  .value=${'// Dark theme example\nconst theme = "dark"'}
167
- .functionSchemas=${this.functionSchemas}
149
+ .functionSchemas=${this.functions}
168
150
  .suggestions=${this.suggestions}
169
151
  dark-theme
170
152
  ></code-editor>
@@ -178,7 +160,7 @@ class CodeEditorDemo extends DemoPage {
178
160
  label="Disabled Editor"
179
161
  supporting-text="This editor is disabled"
180
162
  .value=${'// This editor is disabled\nconst readOnly = true'}
181
- .functionSchemas=${this.functionSchemas}
163
+ .functionSchemas=${this.functions}
182
164
  .suggestions=${this.suggestions}
183
165
  disabled
184
166
  ></code-editor>
@@ -192,23 +174,11 @@ class CodeEditorDemo extends DemoPage {
192
174
  label="Error Editor"
193
175
  supporting-text="This editor has an error"
194
176
  .value=${'// This editor has an error\nconst invalid = true'}
195
- .functionSchemas=${this.functionSchemas}
177
+ .functionSchemas=${this.functions}
196
178
  .suggestions=${this.suggestions}
197
179
  invalid
198
180
  ></code-editor>
199
181
  </section>
200
-
201
- <section class="demo-section">
202
- <h2 class="display-large">Function Schemas</h2>
203
- <p>Available function schemas for autocomplete:</p>
204
- <pre>${JSON.stringify(this.functionSchemas, null, 2)}</pre>
205
- </section>
206
-
207
- <section class="demo-section">
208
- <h2 class="display-large">Suggestions</h2>
209
- <p>Available suggestions (use @mention syntax):</p>
210
- <pre>${JSON.stringify(this.suggestions, null, 2)}</pre>
211
- </section>
212
182
  `
213
183
  }
214
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@api-client/ui",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "Internal UI component library for the API Client ecosystem.",
5
5
  "license": "UNLICENSED",
6
6
  "main": "build/src/index.js",
@@ -15,5 +15,10 @@ declare global {
15
15
  }
16
16
 
17
17
  export default CodeEditorElement
18
- export type { Suggestion, FunctionInsertEvent, SuggestionInsertEvent } from './internals/types.js'
18
+ export type {
19
+ Suggestion,
20
+ FunctionInsertEvent,
21
+ SuggestionInsertEvent,
22
+ FunctionDocumentationEvent,
23
+ } from './internals/types.js'
19
24
  export type { FunctionSchema, FunctionParameter } from '@pawel-up/jexl/schemas/types.js'
@@ -33,7 +33,7 @@ export class ChipWidget extends WidgetType {
33
33
  const pos = view.posAtDOM(wrapper)
34
34
  if (pos === null) return
35
35
 
36
- const originalText = `{{${this.suggestion.label}}}`
36
+ const originalText = `{{${this.suggestion.id}}}`
37
37
  view.dispatch({
38
38
  changes: { from: pos, to: pos + originalText.length, insert: '' },
39
39
  })
@@ -117,8 +117,8 @@ export default css`
117
117
  border: none;
118
118
  }
119
119
 
120
- .cm-focused .cm-cursor {
121
- border-left-color: var(--md-sys-color-primary);
120
+ .cm-focused .cm-content[contenteditable='true'] {
121
+ caret-color: var(--md-sys-color-primary);
122
122
  }
123
123
 
124
124
  ::selection {
@@ -186,6 +186,12 @@ export default css`
186
186
  .param-description {
187
187
  color: var(--md-sys-color-on-surface-variant);
188
188
  }
189
+
190
+ .documentation {
191
+ margin-top: 8px;
192
+ padding-top: 8px;
193
+ border-top: 1px solid var(--md-sys-color-outline-variant);
194
+ }
189
195
  }
190
196
 
191
197
  /* Responsive */
@@ -27,9 +27,11 @@ import {
27
27
  import type { FunctionSchema } from '@pawel-up/jexl/schemas/types.js'
28
28
  import { getTypeStringFromSchema } from '@pawel-up/jexl/schemas/utils.js'
29
29
  import { functionLinter } from './Linter.js'
30
- import type { FunctionInsertEvent, Suggestion, SuggestionInsertEvent } from './types.js'
30
+ import type { FunctionInsertEvent, Suggestion, SuggestionInsertEvent, FunctionDocumentationEvent } from './types.js'
31
31
  import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'
32
32
 
33
+ import '../../../md/button/ui-button.js'
34
+
33
35
  /**
34
36
  * A CodeMirror 6 based editor component that supports function autocomplete and suggestion placeholders.
35
37
  *
@@ -42,6 +44,7 @@ import { SuggestionMatchDecorator } from './SuggestionMatchDecorator.js'
42
44
  *
43
45
  * @fires function-insert - When a function is inserted
44
46
  * @fires suggestion-insert - When a suggestion is inserted
47
+ * @fires function-documentation - When documentation is requested for a function
45
48
  * @fires input - When the editor content changes
46
49
  * @fires change - When the editor loses focus and content has changed
47
50
  */
@@ -143,6 +146,12 @@ export default class CodeEditor extends LitElement {
143
146
  @property({ type: String })
144
147
  accessor language = 'javascript'
145
148
 
149
+ /**
150
+ * Whether to show documentation links/buttons in function tooltips and autocomplete
151
+ */
152
+ @property({ type: Boolean, attribute: 'show-documentation' })
153
+ accessor showDocumentation = false
154
+
146
155
  @query('.editor-container')
147
156
  private accessor editorContainer!: HTMLDivElement
148
157
 
@@ -168,12 +177,12 @@ export default class CodeEditor extends LitElement {
168
177
  */
169
178
  get activeSuggestions(): Suggestion[] {
170
179
  const suggestions: Suggestion[] = []
171
- const placeholderPattern = /\{\{(\w+)\}\}/g
180
+ const placeholderPattern = /\{\{([^}]+)\}\}/g
172
181
  let match: RegExpExecArray | null
173
182
  while ((match = placeholderPattern.exec(this.value)) !== null) {
174
183
  const placeholderText = match[1]
175
- // Find suggestion by label
176
- const suggestion = this.suggestions.find((s) => s.label.toLowerCase() === placeholderText.toLowerCase())
184
+ // Find suggestion by id
185
+ const suggestion = this.suggestions.find((s) => s.id === placeholderText)
177
186
  if (suggestion) {
178
187
  suggestions.push(suggestion)
179
188
  }
@@ -231,6 +240,9 @@ export default class CodeEditor extends LitElement {
231
240
  }
232
241
  if (update.focusChanged) {
233
242
  this.handleFocusChange(update.view.hasFocus)
243
+ } else if (update.view.hasFocus !== this.isEditorFocus) {
244
+ // If focus state changed without focusChanged event, update manually
245
+ this.handleFocusChange(update.view.hasFocus)
234
246
  }
235
247
  }),
236
248
  this.createPlaceholderPlugin(),
@@ -274,7 +286,7 @@ export default class CodeEditor extends LitElement {
274
286
  * This is created as a method to get access to the component's `suggestions`.
275
287
  */
276
288
  private createPlaceholderPlugin(): Extension {
277
- const placeholderMatcher = new SuggestionMatchDecorator(/\{\{(\w+)\}\}/g, this.suggestions)
289
+ const placeholderMatcher = new SuggestionMatchDecorator(/\{\{([^}]+)\}\}/g, this.suggestions)
278
290
  this.placeholderMatcher = placeholderMatcher
279
291
  // This class needs to be defined here to have access to the `placeholderMatcher`
280
292
  class AtomicDecorationRange {
@@ -319,7 +331,7 @@ export default class CodeEditor extends LitElement {
319
331
  }
320
332
 
321
333
  // Check if we're typing a suggestion trigger (e.g., {{)
322
- const suggestionMatch = textBefore.match(/\{\{(\w*)$/)
334
+ const suggestionMatch = textBefore.match(/\{\{([^}]*)$/)
323
335
  if (suggestionMatch) {
324
336
  const prefix = suggestionMatch[1]
325
337
  const suggestions = this.suggestions
@@ -342,6 +354,7 @@ export default class CodeEditor extends LitElement {
342
354
  const result: Completion = {
343
355
  label: schema.name,
344
356
  detail: schema.description || '',
357
+ type: 'function',
345
358
  info: () => this.createFunctionInfoElement(schema),
346
359
  apply: (view: EditorView, completion: unknown, from: number, to: number) => {
347
360
  const functionCall = this.formatFunctionCall(schema)
@@ -361,8 +374,9 @@ export default class CodeEditor extends LitElement {
361
374
  label: suggestion.label,
362
375
  detail: suggestion.description || '',
363
376
  info: suggestion.suffix || '',
377
+ type: 'variable',
364
378
  apply: (view: EditorView, completion: unknown, from: number, to: number) => {
365
- const placeholderText = `{{${suggestion.label}}}`
379
+ const placeholderText = `{{${suggestion.id}}}`
366
380
  view.dispatch({
367
381
  changes: { from: from - 2, to, insert: placeholderText }, // -2 to include the {{
368
382
  selection: { anchor: from - 2 + placeholderText.length },
@@ -403,7 +417,7 @@ export default class CodeEditor extends LitElement {
403
417
  * Creates a styled HTML element to display function documentation.
404
418
  * This is used for both hover tooltips and autocomplete info panels.
405
419
  */
406
- private createFunctionInfoElement(fn: FunctionSchema): HTMLElement {
420
+ protected createFunctionInfoElement(fn: FunctionSchema): HTMLElement {
407
421
  const container = document.createElement('div')
408
422
  container.className = 'function-info' // for styling
409
423
 
@@ -464,6 +478,27 @@ export default class CodeEditor extends LitElement {
464
478
  container.appendChild(returnsContainer)
465
479
  }
466
480
 
481
+ // Add documentation button if showDocumentation is enabled
482
+ if (this.showDocumentation) {
483
+ const docContainer = document.createElement('div')
484
+ docContainer.className = 'documentation'
485
+
486
+ const docButton = document.createElement('ui-button')
487
+ docButton.type = 'button'
488
+ docButton.className = 'documentation-button'
489
+ docButton.textContent = 'Show documentation'
490
+ docButton.setAttribute('aria-label', `Show documentation for ${fn.name} function`)
491
+
492
+ docButton.addEventListener('click', (event) => {
493
+ event.preventDefault()
494
+ event.stopPropagation()
495
+ this.dispatchFunctionDocumentation(fn)
496
+ })
497
+
498
+ docContainer.appendChild(docButton)
499
+ container.appendChild(docContainer)
500
+ }
501
+
467
502
  return container
468
503
  }
469
504
 
@@ -552,6 +587,17 @@ export default class CodeEditor extends LitElement {
552
587
  this.dispatchEvent(event)
553
588
  }
554
589
 
590
+ /**
591
+ * Dispatch function documentation event
592
+ */
593
+ private dispatchFunctionDocumentation(functionSchema: FunctionSchema): void {
594
+ const event = new CustomEvent<FunctionDocumentationEvent>('function-documentation', {
595
+ detail: { functionSchema },
596
+ bubbles: true,
597
+ })
598
+ this.dispatchEvent(event)
599
+ }
600
+
555
601
  /**
556
602
  * Focus the editor
557
603
  */
@@ -1,6 +1,6 @@
1
1
  import { MatchDecorator, Decoration } from '@codemirror/view'
2
2
  import type { Suggestion } from './types.js'
3
- import { ChipWidget } from './PlaceholderWidget.js'
3
+ import { ChipWidget } from './ChipWidget.js'
4
4
 
5
5
  /**
6
6
  * A class that specializes in creating and managing decorations for suggestion matches in a code editor.
@@ -23,12 +23,11 @@ export class SuggestionMatchDecorator extends MatchDecorator {
23
23
  }
24
24
 
25
25
  #decoration(match: RegExpExecArray): Decoration {
26
- const label = match[1]
27
- const suggestion = this.suggestions.find((s) => s.label === label)
26
+ const id = match[1]
27
+ const suggestion = this.suggestions.find((s) => s.id === id)
28
28
 
29
29
  // If no suggestion is found, create a default one
30
- const suggestionData: Suggestion = suggestion || { id: label, label }
31
-
30
+ const suggestionData: Suggestion = suggestion || { id, label: id }
32
31
  return Decoration.replace({
33
32
  widget: new ChipWidget(suggestionData),
34
33
  })
@@ -26,3 +26,8 @@ export interface SuggestionInsertEvent {
26
26
  /** The position where the suggestion was inserted */
27
27
  position: number
28
28
  }
29
+
30
+ export interface FunctionDocumentationEvent {
31
+ /** The function schema for which documentation is requested */
32
+ functionSchema: FunctionSchema
33
+ }