@health-samurai/react-components 0.0.0-alpha.18 → 0.0.0-alpha.20

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bundle.css +51 -33
  3. package/dist/src/components/code-editor/fhir-autocomplete.d.ts +70 -0
  4. package/dist/src/components/code-editor/fhir-autocomplete.d.ts.map +1 -0
  5. package/dist/src/components/code-editor/fhir-autocomplete.js +1849 -0
  6. package/dist/src/components/code-editor/fhir-autocomplete.js.map +1 -0
  7. package/dist/src/components/code-editor/fhir-autocomplete.test.js +1099 -0
  8. package/dist/src/components/code-editor/fhir-autocomplete.test.js.map +1 -0
  9. package/dist/src/components/code-editor/http/index.d.ts +9 -1
  10. package/dist/src/components/code-editor/http/index.d.ts.map +1 -1
  11. package/dist/src/components/code-editor/http/index.js +423 -3
  12. package/dist/src/components/code-editor/http/index.js.map +1 -1
  13. package/dist/src/components/code-editor/index.d.ts +13 -4
  14. package/dist/src/components/code-editor/index.d.ts.map +1 -1
  15. package/dist/src/components/code-editor/index.js +505 -96
  16. package/dist/src/components/code-editor/index.js.map +1 -1
  17. package/dist/src/components/code-editor/json-ast.d.ts +46 -0
  18. package/dist/src/components/code-editor/json-ast.d.ts.map +1 -0
  19. package/dist/src/components/code-editor/json-ast.js +465 -0
  20. package/dist/src/components/code-editor/json-ast.js.map +1 -0
  21. package/dist/src/components/code-editor/json-ast.test.js +206 -0
  22. package/dist/src/components/code-editor/json-ast.test.js.map +1 -0
  23. package/dist/src/components/code-editor/sql-completion.d.ts +22 -0
  24. package/dist/src/components/code-editor/sql-completion.d.ts.map +1 -0
  25. package/dist/src/components/code-editor/sql-completion.js +895 -0
  26. package/dist/src/components/code-editor/sql-completion.js.map +1 -0
  27. package/dist/src/components/date-picker-input.d.ts +10 -0
  28. package/dist/src/components/date-picker-input.d.ts.map +1 -0
  29. package/dist/src/components/date-picker-input.js +90 -0
  30. package/dist/src/components/date-picker-input.js.map +1 -0
  31. package/dist/src/components/date-picker-input.stories.js +76 -0
  32. package/dist/src/components/date-picker-input.stories.js.map +1 -0
  33. package/dist/src/index.d.ts +1 -0
  34. package/dist/src/index.d.ts.map +1 -1
  35. package/dist/src/index.js +1 -0
  36. package/dist/src/index.js.map +1 -1
  37. package/dist/src/shadcn/components/ui/alert-dialog.d.ts +1 -1
  38. package/dist/src/shadcn/components/ui/calendar.d.ts +1 -1
  39. package/dist/src/shadcn/components/ui/carousel.d.ts +1 -1
  40. package/dist/src/shadcn/components/ui/chart.d.ts +3 -3
  41. package/dist/src/shadcn/components/ui/chart.d.ts.map +1 -1
  42. package/dist/src/shadcn/components/ui/chart.js +1 -1
  43. package/dist/src/shadcn/components/ui/chart.js.map +1 -1
  44. package/dist/src/shadcn/components/ui/command.d.ts +1 -1
  45. package/dist/src/shadcn/components/ui/pagination.d.ts +1 -1
  46. package/dist/src/shadcn/components/ui/resizable.stories.js +2 -2
  47. package/dist/src/shadcn/components/ui/resizable.stories.js.map +1 -1
  48. package/dist/src/shadcn/components/ui/sidebar.d.ts +4 -4
  49. package/dist/src/shadcn/components/ui/tabs.d.ts +3 -1
  50. package/dist/src/shadcn/components/ui/tabs.d.ts.map +1 -1
  51. package/dist/src/shadcn/components/ui/tabs.js +129 -2
  52. package/dist/src/shadcn/components/ui/tabs.js.map +1 -1
  53. package/dist/src/shadcn/components/ui/tabs.stories.js +1 -1
  54. package/dist/src/shadcn/components/ui/tabs.stories.js.map +1 -1
  55. package/dist/src/shadcn/components/ui/toggle-group.d.ts +1 -1
  56. package/dist/src/typography.css +1 -1
  57. package/package.json +24 -19
  58. package/src/components/code-editor/fhir-autocomplete.test.ts +993 -0
  59. package/src/components/code-editor/fhir-autocomplete.ts +2321 -0
  60. package/src/components/code-editor/http/index.ts +339 -2
  61. package/src/components/code-editor/index.tsx +593 -102
  62. package/src/components/code-editor/json-ast.test.ts +230 -0
  63. package/src/components/code-editor/json-ast.ts +590 -0
  64. package/src/components/code-editor/sql-completion.ts +1105 -0
  65. package/src/components/date-picker-input.stories.tsx +79 -0
  66. package/src/components/date-picker-input.tsx +104 -0
  67. package/src/index.tsx +1 -0
  68. package/src/shadcn/components/ui/chart.tsx +6 -3
  69. package/src/shadcn/components/ui/resizable.stories.tsx +2 -2
  70. package/src/shadcn/components/ui/tabs.stories.tsx +1 -1
  71. package/src/shadcn/components/ui/tabs.tsx +160 -2
  72. package/src/typography.css +1 -1
  73. package/dist/src/components/code-editor/http/grammar/http.test.d.ts +0 -2
  74. package/dist/src/components/code-editor/http/grammar/http.test.d.ts.map +0 -1
@@ -1,21 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";
2
+ import { acceptCompletion, autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap, completionStatus, moveCompletionSelection } from "@codemirror/autocomplete";
3
3
  import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
4
4
  import { json, jsonParseLinter } from "@codemirror/lang-json";
5
5
  import { SQLDialect, sql } from "@codemirror/lang-sql";
6
6
  import { yaml } from "@codemirror/lang-yaml";
7
- import { bracketMatching, foldGutter, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting } from "@codemirror/language";
8
- import { linter, lintGutter, lintKeymap } from "@codemirror/lint";
7
+ import { bracketMatching, foldGutter, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from "@codemirror/language";
8
+ import { linter, lintKeymap } from "@codemirror/lint";
9
9
  import { closeSearchPanel, findNext, findPrevious, getSearchQuery, highlightSelectionMatches, SearchQuery, search, searchKeymap, setSearchQuery } from "@codemirror/search";
10
- import { Compartment, EditorState, RangeSet, StateEffect, StateField } from "@codemirror/state";
11
- import { crosshairCursor, Decoration, drawSelection, dropCursor, EditorView, GutterMarker, gutterLineClass, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
10
+ import { Compartment, EditorState, Prec, RangeSet, StateEffect, StateField } from "@codemirror/state";
11
+ import { crosshairCursor, Decoration, drawSelection, dropCursor, EditorView, GutterMarker, gutterLineClass, highlightSpecialChars, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
12
12
  import { tags } from "@lezer/highlight";
13
- import { Braces, ChevronDown, ChevronUp, Terminal, X } from "lucide-react";
13
+ import { vim } from "@replit/codemirror-vim";
14
+ import { ChevronDown, ChevronsRight, ChevronUp, Heading, Table2, Terminal, X } from "lucide-react";
14
15
  import * as React from "react";
15
16
  import { flushSync } from "react-dom";
16
17
  import { createRoot } from "react-dom/client";
17
- import { ComplexTypeIcon, SquareFunctionIcon, TypCodeIcon } from "../../icons.js";
18
+ import { ComplexTypeIcon, ResourceIcon, SquareFunctionIcon, TypCodeIcon } from "../../icons.js";
19
+ import { buildFhirCompletionExtension, fhirDiagnosticsField } from "./fhir-autocomplete.js";
18
20
  import { http } from "./http/index.js";
21
+ import { buildSqlCompletionExtensions, fetchSqlMetadata } from "./sql-completion.js";
19
22
  class ErrorLineGutterMarker extends GutterMarker {
20
23
  elementClass = "cm-errorLineGutter";
21
24
  }
@@ -25,33 +28,80 @@ const errorLineDecoration = Decoration.line({
25
28
  });
26
29
  const setIssueLinesEffect = StateEffect.define();
27
30
  let errorTooltipEl = null;
28
- function showErrorTooltip(anchor, message) {
29
- hideErrorTooltip();
30
- const tooltip = document.createElement("div");
31
- tooltip.textContent = message;
32
- Object.assign(tooltip.style, {
33
- position: "fixed",
31
+ function formatErrorTypeTitle(code) {
32
+ return code.split("-").map((w)=>w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
33
+ }
34
+ function renderErrorCard(msg) {
35
+ const card = document.createElement("div");
36
+ Object.assign(card.style, {
34
37
  backgroundColor: "var(--color-bg-primary)",
35
38
  border: "1px solid var(--color-border-primary)",
36
39
  borderRadius: "var(--radius-md)",
37
40
  padding: "6px 10px",
41
+ boxShadow: "0 2px 6px rgba(0, 0, 0, 0.08)"
42
+ });
43
+ const newlineIdx = msg.indexOf("\n");
44
+ if (newlineIdx !== -1) {
45
+ const title = msg.slice(0, newlineIdx);
46
+ const body = msg.slice(newlineIdx + 1);
47
+ const titleEl = document.createElement("div");
48
+ titleEl.textContent = formatErrorTypeTitle(title);
49
+ Object.assign(titleEl.style, {
50
+ fontWeight: "600"
51
+ });
52
+ const hr = document.createElement("div");
53
+ Object.assign(hr.style, {
54
+ borderTop: "1px solid var(--color-border-primary)",
55
+ margin: "4px 0"
56
+ });
57
+ const bodyEl = document.createElement("div");
58
+ bodyEl.textContent = body;
59
+ Object.assign(bodyEl.style, {
60
+ whiteSpace: "pre-wrap"
61
+ });
62
+ card.append(titleEl, hr, bodyEl);
63
+ } else {
64
+ card.textContent = msg;
65
+ card.style.whiteSpace = "pre-wrap";
66
+ }
67
+ return card;
68
+ }
69
+ function showErrorTooltip(message, x, y) {
70
+ hideErrorTooltip();
71
+ const tooltip = document.createElement("div");
72
+ Object.assign(tooltip.style, {
73
+ position: "fixed",
38
74
  fontSize: "12px",
39
75
  lineHeight: "1.4",
40
76
  color: "var(--color-text-error-primary)",
41
77
  fontFamily: "var(--font-family-sans)",
42
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
43
78
  zIndex: "1000",
44
79
  pointerEvents: "none",
45
80
  maxWidth: "400px",
46
- whiteSpace: "pre-wrap"
81
+ display: "flex",
82
+ flexDirection: "column",
83
+ gap: "6px"
47
84
  });
85
+ const parts = message.split("\n\x00\n");
86
+ for (const part of parts){
87
+ tooltip.append(renderErrorCard(part ?? ""));
88
+ }
48
89
  document.body.appendChild(tooltip);
49
90
  errorTooltipEl = tooltip;
50
- const guttersEl = anchor.closest(".cm-gutters");
51
- const guttersRect = guttersEl ? guttersEl.getBoundingClientRect() : anchor.getBoundingClientRect();
52
- const anchorRect = anchor.getBoundingClientRect();
53
- tooltip.style.left = `${guttersRect.right + 4}px`;
54
- tooltip.style.top = `${anchorRect.top}px`;
91
+ const tooltipRect = tooltip.getBoundingClientRect();
92
+ let top = y - tooltipRect.height - 8;
93
+ // If tooltip goes above viewport, show below cursor instead
94
+ if (top < 4) {
95
+ top = y + 20;
96
+ }
97
+ // If it still goes below viewport, clamp to bottom
98
+ if (top + tooltipRect.height > window.innerHeight - 4) {
99
+ top = window.innerHeight - tooltipRect.height - 4;
100
+ }
101
+ // Final clamp to top
102
+ if (top < 4) top = 4;
103
+ tooltip.style.left = `${x}px`;
104
+ tooltip.style.top = `${top}px`;
55
105
  }
56
106
  function hideErrorTooltip() {
57
107
  errorTooltipEl?.remove();
@@ -89,6 +139,21 @@ const issueLinesField = StateField.define({
89
139
  };
90
140
  }
91
141
  }
142
+ if (tr.docChanged) {
143
+ try {
144
+ return {
145
+ gutterMarkers: state.gutterMarkers.map(tr.changes),
146
+ lineDecorations: state.lineDecorations.map(tr.changes),
147
+ messages: state.messages
148
+ };
149
+ } catch {
150
+ return {
151
+ gutterMarkers: RangeSet.empty,
152
+ lineDecorations: Decoration.none,
153
+ messages: new Map()
154
+ };
155
+ }
156
+ }
92
157
  return state;
93
158
  },
94
159
  provide (field) {
@@ -98,28 +163,49 @@ const issueLinesField = StateField.define({
98
163
  ];
99
164
  }
100
165
  });
101
- const errorTooltipHandler = EditorView.domEventHandlers({
102
- mouseover (event, view) {
103
- const target = event.target;
104
- const gutterEl = target.closest(".cm-lineNumbers .cm-gutterElement");
105
- if (!gutterEl) {
106
- hideErrorTooltip();
107
- return false;
108
- }
166
+ function getErrorMessageForLine(view, lineNo) {
167
+ const issueMsg = view.state.field(issueLinesField).messages.get(lineNo);
168
+ if (issueMsg) return issueMsg;
169
+ try {
170
+ return view.state.field(fhirDiagnosticsField).messages.get(lineNo);
171
+ } catch {
172
+ return undefined;
173
+ }
174
+ }
175
+ function handleErrorTooltipMove(event, view) {
176
+ const target = event.target;
177
+ const mouseEvent = event;
178
+ // Check gutter line number
179
+ const gutterEl = target.closest(".cm-lineNumbers .cm-gutterElement");
180
+ if (gutterEl) {
109
181
  const lineNo = Number.parseInt(gutterEl.textContent ?? "", 10);
110
- if (Number.isNaN(lineNo)) {
111
- hideErrorTooltip();
112
- return false;
182
+ if (!Number.isNaN(lineNo)) {
183
+ const message = getErrorMessageForLine(view, lineNo);
184
+ if (message) {
185
+ showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY);
186
+ return false;
187
+ }
113
188
  }
114
- const { messages } = view.state.field(issueLinesField);
115
- const message = messages.get(lineNo);
116
- if (!message) {
117
- hideErrorTooltip();
189
+ hideErrorTooltip();
190
+ return false;
191
+ }
192
+ // Check content line (cm-line) — follow cursor
193
+ const lineEl = target.closest(".cm-line");
194
+ if (lineEl) {
195
+ const pos = view.posAtDOM(lineEl);
196
+ const lineNo = view.state.doc.lineAt(pos).number;
197
+ const message = getErrorMessageForLine(view, lineNo);
198
+ if (message) {
199
+ showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY);
118
200
  return false;
119
201
  }
120
- showErrorTooltip(gutterEl, message);
121
- return false;
122
- },
202
+ }
203
+ hideErrorTooltip();
204
+ return false;
205
+ }
206
+ const errorTooltipHandler = EditorView.domEventHandlers({
207
+ mouseover: handleErrorTooltipMove,
208
+ mousemove: handleErrorTooltipMove,
123
209
  mouseleave () {
124
210
  hideErrorTooltip();
125
211
  return false;
@@ -155,23 +241,38 @@ const baseTheme = EditorView.theme({
155
241
  fontFamily: "var(--font-family-mono)"
156
242
  },
157
243
  ".cm-gutters": {
158
- backgroundColor: "var(--color-bg-primary)",
244
+ backgroundColor: "transparent",
159
245
  border: "none"
160
246
  },
161
247
  ".cm-lineNumbers": {
162
- paddingLeft: "16px"
248
+ minWidth: "3.5ch"
163
249
  },
164
- ".cm-activeLineGutter": {
250
+ ".cm-lineNumbers .cm-gutterElement": {
251
+ minWidth: "3.5ch",
252
+ paddingRight: "4px",
253
+ color: "var(--color-text-quaternary)"
254
+ },
255
+ ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": {
165
256
  backgroundColor: "var(--color-bg-primary)",
166
- color: "var(--color-text-primary)"
257
+ color: "var(--color-text-secondary)"
258
+ },
259
+ ".cm-activeLineGutter": {
260
+ backgroundColor: "transparent !important"
167
261
  },
168
262
  ".cm-activeLine": {
169
- backgroundColor: "rgba(255, 255, 255, 0)"
263
+ backgroundColor: "transparent !important"
170
264
  },
171
- ".cm-errorLineGutter": {
265
+ ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": {
172
266
  color: "var(--color-text-error-primary)",
173
267
  backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)"
174
268
  },
269
+ ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": {
270
+ color: "var(--color-text-error-primary)",
271
+ backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
272
+ display: "flex",
273
+ alignItems: "center",
274
+ justifyContent: "center"
275
+ },
175
276
  ".cm-errorLine": {
176
277
  backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)"
177
278
  }
@@ -187,7 +288,15 @@ const completionTheme = EditorView.theme({
187
288
  },
188
289
  ".cm-completionLabel": {
189
290
  flex: "1",
190
- minWidth: "0"
291
+ minWidth: "0",
292
+ fontFamily: "var(--font-family-mono)",
293
+ fontSize: "var(--font-size-sm)",
294
+ lineHeight: "var(--font-leading-5)"
295
+ },
296
+ ".cm-completionMatchedText": {
297
+ textDecoration: "none",
298
+ fontWeight: "600",
299
+ color: "var(--color-text-link)"
191
300
  },
192
301
  ".cm-completionDetail": {
193
302
  color: "var(--color-text-tertiary)",
@@ -200,9 +309,10 @@ const completionTheme = EditorView.theme({
200
309
  border: "1px solid var(--color-border-primary)",
201
310
  borderRadius: "var(--radius-md)",
202
311
  color: "var(--color-text-secondary)",
203
- fontFamily: "var(--font-family-sans)",
204
- fontSize: "12px",
312
+ fontFamily: "var(--font-family-mono)",
313
+ fontSize: "14px",
205
314
  padding: "8px 12px",
315
+ marginLeft: "8px",
206
316
  lineHeight: "1.4",
207
317
  whiteSpace: "normal",
208
318
  maxWidth: "300px"
@@ -247,19 +357,34 @@ const readOnlyTheme = EditorView.theme({
247
357
  border: "none"
248
358
  },
249
359
  ".cm-lineNumbers": {
250
- paddingLeft: "16px"
360
+ minWidth: "3.5ch"
251
361
  },
252
- ".cm-activeLineGutter": {
362
+ ".cm-lineNumbers .cm-gutterElement": {
363
+ minWidth: "3.5ch",
364
+ paddingRight: "4px",
365
+ color: "var(--color-text-quaternary)"
366
+ },
367
+ ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": {
253
368
  backgroundColor: "var(--color-bg-secondary)",
254
- color: "var(--color-text-primary)"
369
+ color: "var(--color-text-secondary)"
370
+ },
371
+ ".cm-activeLineGutter": {
372
+ backgroundColor: "transparent !important"
255
373
  },
256
374
  ".cm-activeLine": {
257
- backgroundColor: "rgba(255, 255, 255, 0)"
375
+ backgroundColor: "transparent !important"
258
376
  },
259
- ".cm-errorLineGutter": {
377
+ ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": {
260
378
  color: "var(--color-text-error-primary)",
261
379
  backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)"
262
380
  },
381
+ ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": {
382
+ color: "var(--color-text-error-primary)",
383
+ backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
384
+ display: "flex",
385
+ alignItems: "center",
386
+ justifyContent: "center"
387
+ },
263
388
  ".cm-errorLine": {
264
389
  backgroundColor: "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)"
265
390
  }
@@ -336,6 +461,7 @@ function createSearchPanel(view) {
336
461
  alignItems: "center",
337
462
  gap: "2px",
338
463
  padding: "6px 8px",
464
+ marginTop: "4px",
339
465
  backgroundColor: "var(--color-bg-primary)",
340
466
  border: "1px solid var(--color-border-primary)",
341
467
  borderRadius: "var(--radius-md)",
@@ -460,10 +586,10 @@ function createSearchPanel(view) {
460
586
  }
461
587
  };
462
588
  }
463
- const searchPanelTheme = EditorView.baseTheme({
464
- ".cm-panels-top": {
589
+ const searchPanelTheme = EditorView.theme({
590
+ "& .cm-panels-top": {
465
591
  position: "absolute",
466
- top: "4px",
592
+ top: "8px",
467
593
  right: "4px",
468
594
  left: "auto",
469
595
  zIndex: "10",
@@ -471,10 +597,13 @@ const searchPanelTheme = EditorView.baseTheme({
471
597
  border: "none"
472
598
  },
473
599
  ".cm-searchMatch": {
474
- backgroundColor: "#e9f2fc"
600
+ backgroundColor: "var(--color-blue-200) !important"
475
601
  },
476
602
  ".cm-searchMatch-selected": {
477
- backgroundColor: "#d0e2f8"
603
+ backgroundColor: "var(--color-blue-400) !important"
604
+ },
605
+ ".cm-selectionMatch": {
606
+ backgroundColor: "var(--color-blue-100) !important"
478
607
  }
479
608
  });
480
609
  const customSearchExtension = [
@@ -626,13 +755,89 @@ const customSQLDialect = SQLDialect.define({
626
755
  keywords: SQL_KEYWORDS.join(" "),
627
756
  builtin: SQL_BUILTIN.join(" ")
628
757
  });
629
- function languageExtensions(mode, sqlExtraBuiltins) {
758
+ function computeYamlNewlineIndent(lineText) {
759
+ const indent = lineText.match(/^(\s*)/)?.[1] ?? "";
760
+ const trimmed = lineText.trimEnd();
761
+ if (trimmed.endsWith(":")) {
762
+ // After "key:" with no value — increase indent
763
+ // For " - key:", base indent is at the dash content level
764
+ const dashMatch = trimmed.match(/^(\s*-\s+)/);
765
+ const baseIndent = dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent;
766
+ return `${baseIndent} `;
767
+ }
768
+ if (/^\s*-\s*$/.test(trimmed)) {
769
+ // After bare "- " (array item marker only) — align to content after dash
770
+ const dashMatch = trimmed.match(/^(\s*-\s*)/);
771
+ return dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent;
772
+ }
773
+ // Preserve current indent; for " - key: val" align to key level
774
+ const dashKeyMatch = trimmed.match(/^(\s*-\s+)\S/);
775
+ return dashKeyMatch?.[1] ? " ".repeat(dashKeyMatch[1].length) : indent;
776
+ }
777
+ function yamlEnterKeymap() {
778
+ return keymap.of([
779
+ {
780
+ key: "Enter",
781
+ run: (view)=>{
782
+ const { state } = view;
783
+ const pos = state.selection.main.head;
784
+ const line = state.doc.lineAt(pos);
785
+ const newIndent = computeYamlNewlineIndent(line.text);
786
+ view.dispatch({
787
+ changes: {
788
+ from: pos,
789
+ insert: `\n${newIndent}`
790
+ },
791
+ selection: {
792
+ anchor: pos + 1 + newIndent.length
793
+ }
794
+ });
795
+ return true;
796
+ }
797
+ }
798
+ ]);
799
+ }
800
+ function httpYamlEnterKeymap() {
801
+ return keymap.of([
802
+ {
803
+ key: "Enter",
804
+ run: (view)=>{
805
+ const { state } = view;
806
+ const pos = state.selection.main.head;
807
+ const doc = state.doc.toString();
808
+ // Only handle if cursor is in YAML body (after blank line separator)
809
+ const textBeforeCursor = doc.slice(0, pos);
810
+ const blankLineIdx = textBeforeCursor.indexOf("\n\n");
811
+ if (blankLineIdx === -1 || pos <= blankLineIdx + 1) return false;
812
+ // Check if the body looks like YAML (not JSON)
813
+ const bodyStart = blankLineIdx + 2;
814
+ const bodyPrefix = doc.slice(bodyStart, bodyStart + 20).trimStart();
815
+ if (bodyPrefix.startsWith("{") || bodyPrefix.startsWith("[")) return false;
816
+ const line = state.doc.lineAt(pos);
817
+ const newIndent = computeYamlNewlineIndent(line.text);
818
+ view.dispatch({
819
+ changes: {
820
+ from: pos,
821
+ insert: `\n${newIndent}`
822
+ },
823
+ selection: {
824
+ anchor: pos + 1 + newIndent.length
825
+ }
826
+ });
827
+ return true;
828
+ }
829
+ }
830
+ ]);
831
+ }
832
+ function languageExtensions(mode, sqlExtraBuiltins, getUrlSuggestions) {
630
833
  if (mode === "http") {
631
834
  const jsonLang = json();
632
835
  const yamlLang = yaml();
633
836
  return [
634
- http((ct)=>ct === "application/json" ? jsonLang.language : ct === "text/yaml" || ct === "application/yaml" || ct === "application/x-yaml" ? yamlLang.language : null),
635
- syntaxHighlighting(customHighlightStyle)
837
+ http((ct)=>ct === "application/json" ? jsonLang.language : ct === "text/yaml" || ct === "application/yaml" || ct === "application/x-yaml" ? yamlLang.language : null, getUrlSuggestions),
838
+ syntaxHighlighting(customHighlightStyle),
839
+ jsonAutoExpandBraces(),
840
+ httpYamlEnterKeymap()
636
841
  ];
637
842
  } else if (mode === "sql") {
638
843
  let dialect = customSQLDialect;
@@ -654,21 +859,86 @@ function languageExtensions(mode, sqlExtraBuiltins) {
654
859
  } else if (mode === "yaml") {
655
860
  return [
656
861
  yaml(),
657
- syntaxHighlighting(customHighlightStyle)
862
+ syntaxHighlighting(customHighlightStyle),
863
+ yamlEnterKeymap()
658
864
  ];
659
865
  } else {
660
866
  return [
661
867
  json(),
662
- linter(jsonParseLinter(), {
868
+ linter((view)=>{
869
+ if (!view.state.doc.toString().trim()) return [];
870
+ return jsonParseLinter()(view);
871
+ }, {
663
872
  delay: 300
664
873
  }),
665
- syntaxHighlighting(customHighlightStyle)
874
+ syntaxHighlighting(customHighlightStyle),
875
+ jsonAutoExpandBraces()
666
876
  ];
667
877
  }
668
878
  }
669
- export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, viewCallback, readOnly = false, id, mode = "json", isReadOnlyTheme = false, additionalExtensions, issueLineNumbers, foldGutter: enableFoldGutter = true, lintGutter: enableLintGutter = true, lineNumbers: enableLineNumbers = true, sqlExtraBuiltins }) {
879
+ function jsonAutoExpandBraces() {
880
+ return EditorState.transactionFilter.of((tr)=>{
881
+ if (!tr.docChanged) return tr;
882
+ let braceFrom = -1;
883
+ let braceTo = -1;
884
+ let changeCount = 0;
885
+ tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted)=>{
886
+ changeCount++;
887
+ if (inserted.toString() === "{}") {
888
+ braceFrom = fromA;
889
+ braceTo = toA;
890
+ }
891
+ });
892
+ if (changeCount !== 1 || braceFrom === -1) return tr;
893
+ const tree = syntaxTree(tr.startState);
894
+ const nodeBefore = tree.resolveInner(braceFrom, -1);
895
+ if (nodeBefore.name === "String" || nodeBefore.parent?.name === "String") {
896
+ return tr;
897
+ }
898
+ const line = tr.startState.doc.lineAt(braceFrom);
899
+ const indent = line.text.match(/^(\s*)/)?.[1] ?? "";
900
+ const inner = `${indent} `;
901
+ // Check if { is inside an extension array — insert {"url": ""} snippet
902
+ const docText = tr.startState.doc.toString();
903
+ const textBefore = docText.slice(0, braceFrom);
904
+ const isInExtArray = /"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*$/s.test(textBefore);
905
+ if (isInExtArray) {
906
+ const insert = `{\n${inner}"url": ""\n${indent}}`;
907
+ return {
908
+ changes: {
909
+ from: braceFrom,
910
+ to: braceTo,
911
+ insert
912
+ },
913
+ selection: {
914
+ anchor: braceFrom + insert.lastIndexOf('""') + 1
915
+ }
916
+ };
917
+ }
918
+ return {
919
+ changes: {
920
+ from: braceFrom,
921
+ to: braceTo,
922
+ insert: `{\n${inner}\n${indent}}`
923
+ },
924
+ selection: {
925
+ anchor: braceFrom + 2 + inner.length
926
+ }
927
+ };
928
+ });
929
+ }
930
+ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, viewCallback, readOnly = false, id, mode = "json", isReadOnlyTheme = false, additionalExtensions, issueLineNumbers, foldGutter: enableFoldGutter = true, lineNumbers: enableLineNumbers = true, sql, getStructureDefinitions, resourceTypeHint, expandValueSet, getUrlSuggestions, vimMode = false }) {
670
931
  const domRef = React.useRef(null);
671
932
  const [view, setView] = React.useState(null);
933
+ const safeDispatch = React.useCallback((spec)=>{
934
+ try {
935
+ view?.dispatch(spec);
936
+ } catch {
937
+ // Ignore RangeError from stale decoration positions during reconfigure
938
+ }
939
+ }, [
940
+ view
941
+ ]);
672
942
  const initialValue = React.useRef(defaultValue ?? "");
673
943
  const onChangeComparment = React.useRef(new Compartment());
674
944
  const onUpdateComparment = React.useRef(new Compartment());
@@ -676,6 +946,11 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
676
946
  const readOnlyCompartment = React.useRef(new Compartment());
677
947
  const themeCompartment = React.useRef(new Compartment());
678
948
  const additionalExtensionsCompartment = React.useRef(new Compartment());
949
+ const sqlCompletionCompartment = React.useRef(new Compartment());
950
+ const fhirCompletionCompartment = React.useRef(new Compartment());
951
+ const vimCompartment = React.useRef(new Compartment());
952
+ const [sqlFunctions, setSqlFunctions] = React.useState();
953
+ const executeSqlRef = React.useRef(sql?.executeSql);
679
954
  React.useEffect(()=>{
680
955
  if (!domRef.current) {
681
956
  return;
@@ -685,6 +960,7 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
685
960
  state: EditorState.create({
686
961
  doc: initialValue.current,
687
962
  extensions: [
963
+ vimCompartment.current.of(vimMode ? vim() : []),
688
964
  EditorView.contentAttributes.of({
689
965
  "data-gramm": "false"
690
966
  }),
@@ -693,7 +969,18 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
693
969
  lineNumbers()
694
970
  ] : [],
695
971
  ...enableFoldGutter ? [
696
- foldGutter()
972
+ foldGutter({
973
+ markerDOM: (open)=>{
974
+ const el = document.createElement("span");
975
+ el.style.display = "flex";
976
+ el.style.alignItems = "center";
977
+ el.style.justifyContent = "center";
978
+ el.style.width = "100%";
979
+ el.style.height = "100%";
980
+ el.innerHTML = open ? '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
981
+ return el;
982
+ }
983
+ })
697
984
  ] : [],
698
985
  highlightSpecialChars(),
699
986
  history(),
@@ -707,6 +994,7 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
707
994
  autocompletion({
708
995
  icons: false,
709
996
  maxRenderedOptions: 1000,
997
+ defaultKeymap: false,
710
998
  addToOptions: [
711
999
  {
712
1000
  render: renderCompletionIcon,
@@ -723,29 +1011,56 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
723
1011
  }),
724
1012
  rectangularSelection(),
725
1013
  crosshairCursor(),
726
- highlightActiveLine(),
727
- highlightActiveLineGutter(),
728
1014
  highlightSelectionMatches(),
1015
+ Prec.highest(keymap.of([
1016
+ {
1017
+ key: "Tab",
1018
+ run: (v)=>{
1019
+ if (completionStatus(v.state) === "active") {
1020
+ return moveCompletionSelection(true)(v);
1021
+ }
1022
+ return false;
1023
+ }
1024
+ },
1025
+ {
1026
+ key: "Shift-Tab",
1027
+ run: (v)=>{
1028
+ if (completionStatus(v.state) === "active") {
1029
+ return moveCompletionSelection(false)(v);
1030
+ }
1031
+ return false;
1032
+ }
1033
+ },
1034
+ {
1035
+ key: "Enter",
1036
+ run: (v)=>{
1037
+ if (completionStatus(v.state) === "active") {
1038
+ return acceptCompletion(v);
1039
+ }
1040
+ return false;
1041
+ }
1042
+ }
1043
+ ])),
729
1044
  themeCompartment.current.of(baseTheme),
730
1045
  completionTheme,
731
1046
  keymap.of([
732
1047
  ...closeBracketsKeymap,
1048
+ ...completionKeymap.filter((b)=>b.key !== "Enter"),
733
1049
  ...defaultKeymap,
734
1050
  ...searchKeymap,
735
1051
  ...historyKeymap,
736
1052
  ...foldKeymap,
737
- ...completionKeymap,
738
1053
  ...lintKeymap
739
1054
  ]),
740
- ...enableLintGutter ? [
741
- lintGutter()
742
- ] : [],
743
1055
  issueLinesField,
744
1056
  errorTooltipHandler,
1057
+ EditorView.exceptionSink.of(()=>{}),
745
1058
  ...customSearchExtension,
746
1059
  onChangeComparment.current.of([]),
747
1060
  onUpdateComparment.current.of([]),
748
- additionalExtensionsCompartment.current.of([])
1061
+ additionalExtensionsCompartment.current.of([]),
1062
+ sqlCompletionCompartment.current.of([]),
1063
+ fhirCompletionCompartment.current.of([])
749
1064
  ]
750
1065
  })
751
1066
  });
@@ -757,7 +1072,55 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
757
1072
  }, [
758
1073
  enableFoldGutter,
759
1074
  enableLineNumbers,
760
- enableLintGutter
1075
+ vimMode
1076
+ ]);
1077
+ React.useEffect(()=>{
1078
+ executeSqlRef.current = sql?.executeSql;
1079
+ });
1080
+ React.useEffect(()=>{
1081
+ if (!view || !sql) {
1082
+ if (view) {
1083
+ safeDispatch({
1084
+ effects: sqlCompletionCompartment.current.reconfigure([])
1085
+ });
1086
+ }
1087
+ setSqlFunctions(undefined);
1088
+ return;
1089
+ }
1090
+ let cancelled = false;
1091
+ fetchSqlMetadata(sql.executeSql).then((metadata)=>{
1092
+ if (cancelled) return;
1093
+ setSqlFunctions(metadata.functions);
1094
+ const extensions = buildSqlCompletionExtensions(metadata, (query, type)=>executeSqlRef.current?.(query, type) ?? Promise.resolve([]));
1095
+ safeDispatch({
1096
+ effects: sqlCompletionCompartment.current.reconfigure(extensions)
1097
+ });
1098
+ }).catch(()=>{});
1099
+ return ()=>{
1100
+ cancelled = true;
1101
+ };
1102
+ }, [
1103
+ view,
1104
+ sql,
1105
+ safeDispatch
1106
+ ]);
1107
+ React.useEffect(()=>{
1108
+ if (!view) return;
1109
+ if (getStructureDefinitions) {
1110
+ safeDispatch({
1111
+ effects: fhirCompletionCompartment.current.reconfigure(buildFhirCompletionExtension(getStructureDefinitions, resourceTypeHint, expandValueSet))
1112
+ });
1113
+ } else {
1114
+ safeDispatch({
1115
+ effects: fhirCompletionCompartment.current.reconfigure([])
1116
+ });
1117
+ }
1118
+ }, [
1119
+ view,
1120
+ getStructureDefinitions,
1121
+ resourceTypeHint,
1122
+ expandValueSet,
1123
+ safeDispatch
761
1124
  ]);
762
1125
  React.useEffect(()=>{
763
1126
  if (viewCallback && view) {
@@ -768,7 +1131,7 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
768
1131
  viewCallback
769
1132
  ]);
770
1133
  React.useEffect(()=>{
771
- view?.dispatch({
1134
+ safeDispatch({
772
1135
  effects: onChangeComparment.current.reconfigure([
773
1136
  EditorView.updateListener.of((update)=>{
774
1137
  if (update.docChanged && onChange) {
@@ -778,11 +1141,11 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
778
1141
  ])
779
1142
  });
780
1143
  }, [
781
- view,
782
- onChange
1144
+ onChange,
1145
+ safeDispatch
783
1146
  ]);
784
1147
  React.useEffect(()=>{
785
- view?.dispatch({
1148
+ safeDispatch({
786
1149
  effects: onUpdateComparment.current.reconfigure([
787
1150
  EditorView.updateListener.of((update)=>{
788
1151
  if (onUpdate) {
@@ -792,8 +1155,8 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
792
1155
  ])
793
1156
  });
794
1157
  }, [
795
- view,
796
- onUpdate
1158
+ onUpdate,
1159
+ safeDispatch
797
1160
  ]);
798
1161
  // FIXME: it is probably better to have CM manage its state.
799
1162
  React.useEffect(()=>{
@@ -802,7 +1165,7 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
802
1165
  }
803
1166
  const currentDoc = view.state.doc.toString();
804
1167
  if (currentDoc !== currentValue) {
805
- view.dispatch({
1168
+ safeDispatch({
806
1169
  changes: {
807
1170
  from: 0,
808
1171
  to: currentDoc.length,
@@ -812,69 +1175,98 @@ export function CodeEditor({ defaultValue, currentValue, onChange, onUpdate, vie
812
1175
  }
813
1176
  }, [
814
1177
  currentValue,
815
- view
1178
+ view,
1179
+ safeDispatch
1180
+ ]);
1181
+ const getUrlSuggestionsRef = React.useRef(getUrlSuggestions);
1182
+ getUrlSuggestionsRef.current = getUrlSuggestions;
1183
+ const stableGetUrlSuggestions = React.useMemo(()=>{
1184
+ if (!getUrlSuggestions) return undefined;
1185
+ return (path, method)=>getUrlSuggestionsRef.current?.(path, method) ?? [];
1186
+ }, [
1187
+ getUrlSuggestions
816
1188
  ]);
817
1189
  React.useEffect(()=>{
818
1190
  if (view === null) {
819
1191
  return;
820
1192
  }
821
- view.dispatch({
822
- effects: languageCompartment.current.reconfigure(languageExtensions(mode, sqlExtraBuiltins))
1193
+ safeDispatch({
1194
+ effects: languageCompartment.current.reconfigure(languageExtensions(mode, sqlFunctions, stableGetUrlSuggestions))
823
1195
  });
824
1196
  }, [
825
1197
  mode,
826
1198
  view,
827
- sqlExtraBuiltins
1199
+ sqlFunctions,
1200
+ stableGetUrlSuggestions,
1201
+ safeDispatch
828
1202
  ]);
829
1203
  React.useEffect(()=>{
830
1204
  if (view === null) {
831
1205
  return;
832
1206
  }
833
- view.dispatch({
1207
+ safeDispatch({
834
1208
  effects: [
835
1209
  readOnlyCompartment.current.reconfigure(EditorState.readOnly.of(readOnly))
836
1210
  ]
837
1211
  });
838
1212
  }, [
839
1213
  readOnly,
840
- view
1214
+ view,
1215
+ safeDispatch
841
1216
  ]);
842
1217
  React.useEffect(()=>{
843
1218
  if (view === null) {
844
1219
  return;
845
1220
  }
846
- view.dispatch({
1221
+ safeDispatch({
847
1222
  effects: [
848
1223
  themeCompartment.current.reconfigure(isReadOnlyTheme ? readOnlyTheme : baseTheme)
849
1224
  ]
850
1225
  });
851
1226
  }, [
852
1227
  isReadOnlyTheme,
853
- view
1228
+ view,
1229
+ safeDispatch
854
1230
  ]);
855
1231
  React.useEffect(()=>{
856
1232
  if (view === null) {
857
1233
  return;
858
1234
  }
859
- view.dispatch({
1235
+ safeDispatch({
1236
+ effects: [
1237
+ vimCompartment.current.reconfigure(vimMode ? vim() : [])
1238
+ ]
1239
+ });
1240
+ }, [
1241
+ vimMode,
1242
+ view,
1243
+ safeDispatch
1244
+ ]);
1245
+ React.useEffect(()=>{
1246
+ if (view === null) {
1247
+ return;
1248
+ }
1249
+ safeDispatch({
860
1250
  effects: [
861
1251
  additionalExtensionsCompartment.current.reconfigure(additionalExtensions ?? [])
862
1252
  ]
863
1253
  });
864
1254
  }, [
865
1255
  additionalExtensions,
866
- view
1256
+ view,
1257
+ safeDispatch
867
1258
  ]);
868
1259
  React.useEffect(()=>{
869
1260
  if (view === null) {
870
1261
  return;
871
1262
  }
872
- view.dispatch({
1263
+ safeDispatch({
873
1264
  effects: setIssueLinesEffect.of(issueLineNumbers ?? [])
874
1265
  });
875
1266
  }, [
876
1267
  issueLineNumbers,
877
- view
1268
+ view,
1269
+ safeDispatch
878
1270
  ]);
879
1271
  return /*#__PURE__*/ _jsx("div", {
880
1272
  className: "h-full w-full",
@@ -937,7 +1329,15 @@ const KeywordIcon = ()=>/*#__PURE__*/ _jsx(Terminal, {
937
1329
  size: 16,
938
1330
  color: "#717684"
939
1331
  });
940
- const OperatorIcon = ()=>/*#__PURE__*/ _jsx(Braces, {
1332
+ const OperatorIcon = ()=>/*#__PURE__*/ _jsx(ChevronsRight, {
1333
+ size: 16,
1334
+ color: "#717684"
1335
+ });
1336
+ const TableIcon = ()=>/*#__PURE__*/ _jsx(Table2, {
1337
+ size: 16,
1338
+ color: "#717684"
1339
+ });
1340
+ const HeaderIcon = ()=>/*#__PURE__*/ _jsx(Heading, {
941
1341
  size: 16,
942
1342
  color: "#717684"
943
1343
  });
@@ -945,15 +1345,22 @@ function getCompletionIcon(completion) {
945
1345
  if (completion.type === "function") return SquareFunctionIcon;
946
1346
  if (completion.type === "keyword") return KeywordIcon;
947
1347
  if (completion.type === "operator") return OperatorIcon;
1348
+ if (completion.type === "table") return TableIcon;
1349
+ if (completion.type === "header") return HeaderIcon;
1350
+ if (completion.type === "text") return TypCodeIcon;
1351
+ if (completion.type === "type") return ResourceIcon;
1352
+ if (completion.type === "search-param") return null;
948
1353
  const detail = completion.detail;
949
1354
  if (!detail) {
950
1355
  if (completion.type === "variable") return SquareFunctionIcon;
951
- return null;
1356
+ return TypCodeIcon;
952
1357
  }
953
1358
  const typeName = detail.replace(/\[\]$/, "");
954
- if (!typeName) return null;
1359
+ if (!typeName) return TypCodeIcon;
1360
+ // Search param types (TOKEN, REFERENCE) — no icon
1361
+ if (typeName === typeName.toUpperCase()) return null;
955
1362
  const firstChar = typeName[0];
956
- if (!firstChar) return null;
1363
+ if (!firstChar) return TypCodeIcon;
957
1364
  const isComplex = firstChar === firstChar.toUpperCase();
958
1365
  return isComplex ? ComplexTypeIcon : TypCodeIcon;
959
1366
  }
@@ -965,6 +1372,8 @@ function renderCompletionIcon(completion) {
965
1372
  flushSync(()=>{
966
1373
  createRoot(container).render(/*#__PURE__*/ _jsx(Icon, {}));
967
1374
  });
1375
+ } else {
1376
+ container.style.display = "none";
968
1377
  }
969
1378
  return container;
970
1379
  }