@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
@@ -5,6 +5,8 @@ import {
5
5
  closeBrackets,
6
6
  closeBracketsKeymap,
7
7
  completionKeymap,
8
+ completionStatus,
9
+ moveCompletionSelection,
8
10
  } from "@codemirror/autocomplete";
9
11
  import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
10
12
  import { json, jsonParseLinter } from "@codemirror/lang-json";
@@ -17,8 +19,9 @@ import {
17
19
  HighlightStyle,
18
20
  indentOnInput,
19
21
  syntaxHighlighting,
22
+ syntaxTree,
20
23
  } from "@codemirror/language";
21
- import { linter, lintGutter, lintKeymap } from "@codemirror/lint";
24
+ import { linter, lintKeymap } from "@codemirror/lint";
22
25
  import {
23
26
  closeSearchPanel,
24
27
  findNext,
@@ -34,6 +37,7 @@ import {
34
37
  Compartment,
35
38
  EditorState,
36
39
  type Extension,
40
+ Prec,
37
41
  RangeSet,
38
42
  StateEffect,
39
43
  StateField,
@@ -46,8 +50,6 @@ import {
46
50
  EditorView,
47
51
  GutterMarker,
48
52
  gutterLineClass,
49
- highlightActiveLine,
50
- highlightActiveLineGutter,
51
53
  highlightSpecialChars,
52
54
  keymap,
53
55
  lineNumbers,
@@ -55,13 +57,37 @@ import {
55
57
  type ViewUpdate,
56
58
  } from "@codemirror/view";
57
59
  import { tags } from "@lezer/highlight";
58
- import { Braces, ChevronDown, ChevronUp, Terminal, X } from "lucide-react";
60
+ import { vim } from "@replit/codemirror-vim";
61
+ import {
62
+ ChevronDown,
63
+ ChevronsRight,
64
+ ChevronUp,
65
+ Heading,
66
+ Table2,
67
+ Terminal,
68
+ X,
69
+ } from "lucide-react";
59
70
  import * as React from "react";
60
71
  import { flushSync } from "react-dom";
61
72
  import { createRoot } from "react-dom/client";
62
-
63
- import { ComplexTypeIcon, SquareFunctionIcon, TypCodeIcon } from "../../icons";
64
- import { http } from "./http";
73
+ import {
74
+ ComplexTypeIcon,
75
+ ResourceIcon,
76
+ SquareFunctionIcon,
77
+ TypCodeIcon,
78
+ } from "../../icons";
79
+ import {
80
+ buildFhirCompletionExtension,
81
+ type ExpandValueSet,
82
+ fhirDiagnosticsField,
83
+ type GetStructureDefinitions,
84
+ } from "./fhir-autocomplete";
85
+ import { type GetUrlSuggestions, http } from "./http";
86
+ import {
87
+ buildSqlCompletionExtensions,
88
+ fetchSqlMetadata,
89
+ type SqlConfig,
90
+ } from "./sql-completion";
65
91
 
66
92
  // --- Issue lines: gutter highlighting, line background, hover tooltip ---
67
93
 
@@ -77,37 +103,89 @@ const setIssueLinesEffect = StateEffect.define<IssueLine[]>();
77
103
 
78
104
  let errorTooltipEl: HTMLDivElement | null = null;
79
105
 
80
- function showErrorTooltip(anchor: Element, message: string) {
81
- hideErrorTooltip();
106
+ function formatErrorTypeTitle(code: string): string {
107
+ return code
108
+ .split("-")
109
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
110
+ .join(" ");
111
+ }
82
112
 
83
- const tooltip = document.createElement("div");
84
- tooltip.textContent = message;
85
- Object.assign(tooltip.style, {
86
- position: "fixed",
113
+ function renderErrorCard(msg: string): HTMLElement {
114
+ const card = document.createElement("div");
115
+ Object.assign(card.style, {
87
116
  backgroundColor: "var(--color-bg-primary)",
88
117
  border: "1px solid var(--color-border-primary)",
89
118
  borderRadius: "var(--radius-md)",
90
119
  padding: "6px 10px",
120
+ boxShadow: "0 2px 6px rgba(0, 0, 0, 0.08)",
121
+ });
122
+ const newlineIdx = msg.indexOf("\n");
123
+ if (newlineIdx !== -1) {
124
+ const title = msg.slice(0, newlineIdx);
125
+ const body = msg.slice(newlineIdx + 1);
126
+
127
+ const titleEl = document.createElement("div");
128
+ titleEl.textContent = formatErrorTypeTitle(title);
129
+ Object.assign(titleEl.style, { fontWeight: "600" });
130
+
131
+ const hr = document.createElement("div");
132
+ Object.assign(hr.style, {
133
+ borderTop: "1px solid var(--color-border-primary)",
134
+ margin: "4px 0",
135
+ });
136
+
137
+ const bodyEl = document.createElement("div");
138
+ bodyEl.textContent = body;
139
+ Object.assign(bodyEl.style, { whiteSpace: "pre-wrap" });
140
+
141
+ card.append(titleEl, hr, bodyEl);
142
+ } else {
143
+ card.textContent = msg;
144
+ card.style.whiteSpace = "pre-wrap";
145
+ }
146
+ return card;
147
+ }
148
+
149
+ function showErrorTooltip(message: string, x: number, y: number) {
150
+ hideErrorTooltip();
151
+
152
+ const tooltip = document.createElement("div");
153
+ Object.assign(tooltip.style, {
154
+ position: "fixed",
91
155
  fontSize: "12px",
92
156
  lineHeight: "1.4",
93
157
  color: "var(--color-text-error-primary)",
94
158
  fontFamily: "var(--font-family-sans)",
95
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
96
159
  zIndex: "1000",
97
160
  pointerEvents: "none",
98
161
  maxWidth: "400px",
99
- whiteSpace: "pre-wrap",
162
+ display: "flex",
163
+ flexDirection: "column",
164
+ gap: "6px",
100
165
  });
166
+
167
+ const parts = message.split("\n\x00\n");
168
+ for (const part of parts) {
169
+ tooltip.append(renderErrorCard(part ?? ""));
170
+ }
171
+
101
172
  document.body.appendChild(tooltip);
102
173
  errorTooltipEl = tooltip;
103
174
 
104
- const guttersEl = anchor.closest(".cm-gutters");
105
- const guttersRect = guttersEl
106
- ? guttersEl.getBoundingClientRect()
107
- : anchor.getBoundingClientRect();
108
- const anchorRect = anchor.getBoundingClientRect();
109
- tooltip.style.left = `${guttersRect.right + 4}px`;
110
- tooltip.style.top = `${anchorRect.top}px`;
175
+ const tooltipRect = tooltip.getBoundingClientRect();
176
+ let top = y - tooltipRect.height - 8;
177
+ // If tooltip goes above viewport, show below cursor instead
178
+ if (top < 4) {
179
+ top = y + 20;
180
+ }
181
+ // If it still goes below viewport, clamp to bottom
182
+ if (top + tooltipRect.height > window.innerHeight - 4) {
183
+ top = window.innerHeight - tooltipRect.height - 4;
184
+ }
185
+ // Final clamp to top
186
+ if (top < 4) top = 4;
187
+ tooltip.style.left = `${x}px`;
188
+ tooltip.style.top = `${top}px`;
111
189
  }
112
190
 
113
191
  function hideErrorTooltip() {
@@ -157,6 +235,21 @@ const issueLinesField = StateField.define<{
157
235
  };
158
236
  }
159
237
  }
238
+ if (tr.docChanged) {
239
+ try {
240
+ return {
241
+ gutterMarkers: state.gutterMarkers.map(tr.changes),
242
+ lineDecorations: state.lineDecorations.map(tr.changes),
243
+ messages: state.messages,
244
+ };
245
+ } catch {
246
+ return {
247
+ gutterMarkers: RangeSet.empty,
248
+ lineDecorations: Decoration.none,
249
+ messages: new Map(),
250
+ };
251
+ }
252
+ }
160
253
  return state;
161
254
  },
162
255
  provide(field) {
@@ -167,33 +260,59 @@ const issueLinesField = StateField.define<{
167
260
  },
168
261
  });
169
262
 
170
- const errorTooltipHandler = EditorView.domEventHandlers({
171
- mouseover(event, view) {
172
- const target = event.target as HTMLElement;
173
- const gutterEl = target.closest(
174
- ".cm-lineNumbers .cm-gutterElement",
175
- ) as HTMLElement | null;
176
- if (!gutterEl) {
177
- hideErrorTooltip();
178
- return false;
179
- }
263
+ function getErrorMessageForLine(
264
+ view: EditorView,
265
+ lineNo: number,
266
+ ): string | undefined {
267
+ const issueMsg = view.state.field(issueLinesField).messages.get(lineNo);
268
+ if (issueMsg) return issueMsg;
269
+ try {
270
+ return view.state.field(fhirDiagnosticsField).messages.get(lineNo);
271
+ } catch {
272
+ return undefined;
273
+ }
274
+ }
275
+
276
+ function handleErrorTooltipMove(event: Event, view: EditorView) {
277
+ const target = event.target as HTMLElement;
278
+ const mouseEvent = event as MouseEvent;
180
279
 
280
+ // Check gutter line number
281
+ const gutterEl = target.closest(
282
+ ".cm-lineNumbers .cm-gutterElement",
283
+ ) as HTMLElement | null;
284
+ if (gutterEl) {
181
285
  const lineNo = Number.parseInt(gutterEl.textContent ?? "", 10);
182
- if (Number.isNaN(lineNo)) {
183
- hideErrorTooltip();
184
- return false;
286
+ if (!Number.isNaN(lineNo)) {
287
+ const message = getErrorMessageForLine(view, lineNo);
288
+ if (message) {
289
+ showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY);
290
+ return false;
291
+ }
185
292
  }
293
+ hideErrorTooltip();
294
+ return false;
295
+ }
186
296
 
187
- const { messages } = view.state.field(issueLinesField);
188
- const message = messages.get(lineNo);
189
- if (!message) {
190
- hideErrorTooltip();
297
+ // Check content line (cm-line) — follow cursor
298
+ const lineEl = target.closest(".cm-line") as HTMLElement | null;
299
+ if (lineEl) {
300
+ const pos = view.posAtDOM(lineEl);
301
+ const lineNo = view.state.doc.lineAt(pos).number;
302
+ const message = getErrorMessageForLine(view, lineNo);
303
+ if (message) {
304
+ showErrorTooltip(message, mouseEvent.clientX, mouseEvent.clientY);
191
305
  return false;
192
306
  }
307
+ }
193
308
 
194
- showErrorTooltip(gutterEl, message);
195
- return false;
196
- },
309
+ hideErrorTooltip();
310
+ return false;
311
+ }
312
+
313
+ const errorTooltipHandler = EditorView.domEventHandlers({
314
+ mouseover: handleErrorTooltipMove,
315
+ mousemove: handleErrorTooltipMove,
197
316
  mouseleave() {
198
317
  hideErrorTooltip();
199
318
  return false;
@@ -230,24 +349,40 @@ const baseTheme = EditorView.theme({
230
349
  fontFamily: "var(--font-family-mono)",
231
350
  },
232
351
  ".cm-gutters": {
233
- backgroundColor: "var(--color-bg-primary)",
352
+ backgroundColor: "transparent",
234
353
  border: "none",
235
354
  },
236
355
  ".cm-lineNumbers": {
237
- paddingLeft: "16px",
356
+ minWidth: "3.5ch",
238
357
  },
239
- ".cm-activeLineGutter": {
358
+ ".cm-lineNumbers .cm-gutterElement": {
359
+ minWidth: "3.5ch",
360
+ paddingRight: "4px",
361
+ color: "var(--color-text-quaternary)",
362
+ },
363
+ ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": {
240
364
  backgroundColor: "var(--color-bg-primary)",
241
- color: "var(--color-text-primary)",
365
+ color: "var(--color-text-secondary)",
366
+ },
367
+ ".cm-activeLineGutter": {
368
+ backgroundColor: "transparent !important",
242
369
  },
243
370
  ".cm-activeLine": {
244
- backgroundColor: "rgba(255, 255, 255, 0)",
371
+ backgroundColor: "transparent !important",
245
372
  },
246
- ".cm-errorLineGutter": {
373
+ ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": {
247
374
  color: "var(--color-text-error-primary)",
248
375
  backgroundColor:
249
376
  "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
250
377
  },
378
+ ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": {
379
+ color: "var(--color-text-error-primary)",
380
+ backgroundColor:
381
+ "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
382
+ display: "flex",
383
+ alignItems: "center",
384
+ justifyContent: "center",
385
+ },
251
386
  ".cm-errorLine": {
252
387
  backgroundColor:
253
388
  "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
@@ -266,6 +401,14 @@ const completionTheme = EditorView.theme({
266
401
  ".cm-completionLabel": {
267
402
  flex: "1",
268
403
  minWidth: "0",
404
+ fontFamily: "var(--font-family-mono)",
405
+ fontSize: "var(--font-size-sm)",
406
+ lineHeight: "var(--font-leading-5)",
407
+ },
408
+ ".cm-completionMatchedText": {
409
+ textDecoration: "none",
410
+ fontWeight: "600",
411
+ color: "var(--color-text-link)",
269
412
  },
270
413
  ".cm-completionDetail": {
271
414
  color: "var(--color-text-tertiary)",
@@ -278,9 +421,10 @@ const completionTheme = EditorView.theme({
278
421
  border: "1px solid var(--color-border-primary)",
279
422
  borderRadius: "var(--radius-md)",
280
423
  color: "var(--color-text-secondary)",
281
- fontFamily: "var(--font-family-sans)",
282
- fontSize: "12px",
424
+ fontFamily: "var(--font-family-mono)",
425
+ fontSize: "14px",
283
426
  padding: "8px 12px",
427
+ marginLeft: "8px",
284
428
  lineHeight: "1.4",
285
429
  whiteSpace: "normal",
286
430
  maxWidth: "300px",
@@ -326,20 +470,36 @@ const readOnlyTheme = EditorView.theme({
326
470
  border: "none",
327
471
  },
328
472
  ".cm-lineNumbers": {
329
- paddingLeft: "16px",
473
+ minWidth: "3.5ch",
330
474
  },
331
- ".cm-activeLineGutter": {
475
+ ".cm-lineNumbers .cm-gutterElement": {
476
+ minWidth: "3.5ch",
477
+ paddingRight: "4px",
478
+ color: "var(--color-text-quaternary)",
479
+ },
480
+ ".cm-lineNumbers .cm-gutterElement.cm-activeLineGutter": {
332
481
  backgroundColor: "var(--color-bg-secondary)",
333
- color: "var(--color-text-primary)",
482
+ color: "var(--color-text-secondary)",
483
+ },
484
+ ".cm-activeLineGutter": {
485
+ backgroundColor: "transparent !important",
334
486
  },
335
487
  ".cm-activeLine": {
336
- backgroundColor: "rgba(255, 255, 255, 0)",
488
+ backgroundColor: "transparent !important",
337
489
  },
338
- ".cm-errorLineGutter": {
490
+ ".cm-lineNumbers .cm-gutterElement.cm-errorLineGutter": {
339
491
  color: "var(--color-text-error-primary)",
340
492
  backgroundColor:
341
493
  "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
342
494
  },
495
+ ".cm-foldGutter .cm-gutterElement.cm-errorLineGutter": {
496
+ color: "var(--color-text-error-primary)",
497
+ backgroundColor:
498
+ "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
499
+ display: "flex",
500
+ alignItems: "center",
501
+ justifyContent: "center",
502
+ },
343
503
  ".cm-errorLine": {
344
504
  backgroundColor:
345
505
  "color-mix(in srgb, var(--color-text-error-primary) 7%, transparent)",
@@ -426,6 +586,7 @@ function createSearchPanel(view: EditorView) {
426
586
  alignItems: "center",
427
587
  gap: "2px",
428
588
  padding: "6px 8px",
589
+ marginTop: "4px",
429
590
  backgroundColor: "var(--color-bg-primary)",
430
591
  border: "1px solid var(--color-border-primary)",
431
592
  borderRadius: "var(--radius-md)",
@@ -562,10 +723,10 @@ function createSearchPanel(view: EditorView) {
562
723
  };
563
724
  }
564
725
 
565
- const searchPanelTheme = EditorView.baseTheme({
566
- ".cm-panels-top": {
726
+ const searchPanelTheme = EditorView.theme({
727
+ "& .cm-panels-top": {
567
728
  position: "absolute",
568
- top: "4px",
729
+ top: "8px",
569
730
  right: "4px",
570
731
  left: "auto",
571
732
  zIndex: "10",
@@ -573,10 +734,13 @@ const searchPanelTheme = EditorView.baseTheme({
573
734
  border: "none",
574
735
  },
575
736
  ".cm-searchMatch": {
576
- backgroundColor: "#e9f2fc",
737
+ backgroundColor: "var(--color-blue-200) !important",
577
738
  },
578
739
  ".cm-searchMatch-selected": {
579
- backgroundColor: "#d0e2f8",
740
+ backgroundColor: "var(--color-blue-400) !important",
741
+ },
742
+ ".cm-selectionMatch": {
743
+ backgroundColor: "var(--color-blue-100) !important",
580
744
  },
581
745
  });
582
746
 
@@ -690,23 +854,107 @@ const customSQLDialect = SQLDialect.define({
690
854
  builtin: SQL_BUILTIN.join(" "),
691
855
  });
692
856
 
857
+ function computeYamlNewlineIndent(lineText: string): string {
858
+ const indent = lineText.match(/^(\s*)/)?.[1] ?? "";
859
+ const trimmed = lineText.trimEnd();
860
+
861
+ if (trimmed.endsWith(":")) {
862
+ // After "key:" with no value — increase indent
863
+ // For " - key:", base indent is at the dash content level
864
+ const dashMatch = trimmed.match(/^(\s*-\s+)/);
865
+ const baseIndent = dashMatch?.[1]
866
+ ? " ".repeat(dashMatch[1].length)
867
+ : indent;
868
+ return `${baseIndent} `;
869
+ }
870
+ if (/^\s*-\s*$/.test(trimmed)) {
871
+ // After bare "- " (array item marker only) — align to content after dash
872
+ const dashMatch = trimmed.match(/^(\s*-\s*)/);
873
+ return dashMatch?.[1] ? " ".repeat(dashMatch[1].length) : indent;
874
+ }
875
+ // Preserve current indent; for " - key: val" align to key level
876
+ const dashKeyMatch = trimmed.match(/^(\s*-\s+)\S/);
877
+ return dashKeyMatch?.[1] ? " ".repeat(dashKeyMatch[1].length) : indent;
878
+ }
879
+
880
+ function yamlEnterKeymap(): Extension {
881
+ return keymap.of([
882
+ {
883
+ key: "Enter",
884
+ run: (view) => {
885
+ const { state } = view;
886
+ const pos = state.selection.main.head;
887
+ const line = state.doc.lineAt(pos);
888
+ const newIndent = computeYamlNewlineIndent(line.text);
889
+
890
+ view.dispatch({
891
+ changes: { from: pos, insert: `\n${newIndent}` },
892
+ selection: { anchor: pos + 1 + newIndent.length },
893
+ });
894
+ return true;
895
+ },
896
+ },
897
+ ]);
898
+ }
899
+
900
+ function httpYamlEnterKeymap(): Extension {
901
+ return keymap.of([
902
+ {
903
+ key: "Enter",
904
+ run: (view) => {
905
+ const { state } = view;
906
+ const pos = state.selection.main.head;
907
+ const doc = state.doc.toString();
908
+
909
+ // Only handle if cursor is in YAML body (after blank line separator)
910
+ const textBeforeCursor = doc.slice(0, pos);
911
+ const blankLineIdx = textBeforeCursor.indexOf("\n\n");
912
+ if (blankLineIdx === -1 || pos <= blankLineIdx + 1) return false;
913
+
914
+ // Check if the body looks like YAML (not JSON)
915
+ const bodyStart = blankLineIdx + 2;
916
+ const bodyPrefix = doc.slice(bodyStart, bodyStart + 20).trimStart();
917
+ if (bodyPrefix.startsWith("{") || bodyPrefix.startsWith("["))
918
+ return false;
919
+
920
+ const line = state.doc.lineAt(pos);
921
+ const newIndent = computeYamlNewlineIndent(line.text);
922
+
923
+ view.dispatch({
924
+ changes: { from: pos, insert: `\n${newIndent}` },
925
+ selection: { anchor: pos + 1 + newIndent.length },
926
+ });
927
+ return true;
928
+ },
929
+ },
930
+ ]);
931
+ }
932
+
693
933
  type LanguageMode = "json" | "http" | "sql" | "yaml";
694
934
 
695
- function languageExtensions(mode: LanguageMode, sqlExtraBuiltins?: string[]) {
935
+ function languageExtensions(
936
+ mode: LanguageMode,
937
+ sqlExtraBuiltins?: string[],
938
+ getUrlSuggestions?: GetUrlSuggestions,
939
+ ) {
696
940
  if (mode === "http") {
697
941
  const jsonLang = json();
698
942
  const yamlLang = yaml();
699
943
  return [
700
- http((ct) =>
701
- ct === "application/json"
702
- ? jsonLang.language
703
- : ct === "text/yaml" ||
704
- ct === "application/yaml" ||
705
- ct === "application/x-yaml"
706
- ? yamlLang.language
707
- : null,
944
+ http(
945
+ (ct) =>
946
+ ct === "application/json"
947
+ ? jsonLang.language
948
+ : ct === "text/yaml" ||
949
+ ct === "application/yaml" ||
950
+ ct === "application/x-yaml"
951
+ ? yamlLang.language
952
+ : null,
953
+ getUrlSuggestions,
708
954
  ),
709
955
  syntaxHighlighting(customHighlightStyle),
956
+ jsonAutoExpandBraces(),
957
+ httpYamlEnterKeymap(),
710
958
  ];
711
959
  } else if (mode === "sql") {
712
960
  let dialect = customSQLDialect;
@@ -718,16 +966,81 @@ function languageExtensions(mode: LanguageMode, sqlExtraBuiltins?: string[]) {
718
966
  }
719
967
  return [sql({ dialect }), syntaxHighlighting(customHighlightStyle)];
720
968
  } else if (mode === "yaml") {
721
- return [yaml(), syntaxHighlighting(customHighlightStyle)];
969
+ return [
970
+ yaml(),
971
+ syntaxHighlighting(customHighlightStyle),
972
+ yamlEnterKeymap(),
973
+ ];
722
974
  } else {
723
975
  return [
724
976
  json(),
725
- linter(jsonParseLinter(), { delay: 300 }),
977
+ linter(
978
+ (view) => {
979
+ if (!view.state.doc.toString().trim()) return [];
980
+ return jsonParseLinter()(view);
981
+ },
982
+ { delay: 300 },
983
+ ),
726
984
  syntaxHighlighting(customHighlightStyle),
985
+ jsonAutoExpandBraces(),
727
986
  ];
728
987
  }
729
988
  }
730
989
 
990
+ function jsonAutoExpandBraces(): Extension {
991
+ return EditorState.transactionFilter.of((tr) => {
992
+ if (!tr.docChanged) return tr;
993
+
994
+ let braceFrom = -1;
995
+ let braceTo = -1;
996
+ let changeCount = 0;
997
+
998
+ tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => {
999
+ changeCount++;
1000
+ if (inserted.toString() === "{}") {
1001
+ braceFrom = fromA;
1002
+ braceTo = toA;
1003
+ }
1004
+ });
1005
+
1006
+ if (changeCount !== 1 || braceFrom === -1) return tr;
1007
+
1008
+ const tree = syntaxTree(tr.startState);
1009
+ const nodeBefore = tree.resolveInner(braceFrom, -1);
1010
+ if (nodeBefore.name === "String" || nodeBefore.parent?.name === "String") {
1011
+ return tr;
1012
+ }
1013
+
1014
+ const line = tr.startState.doc.lineAt(braceFrom);
1015
+ const indent = line.text.match(/^(\s*)/)?.[1] ?? "";
1016
+ const inner = `${indent} `;
1017
+
1018
+ // Check if { is inside an extension array — insert {"url": ""} snippet
1019
+ const docText = tr.startState.doc.toString();
1020
+ const textBefore = docText.slice(0, braceFrom);
1021
+ const isInExtArray =
1022
+ /"(?:extension|modifierExtension)"\s*:\s*\[\s*(?:\{[\s\S]*?\}\s*,?\s*)*$/s.test(
1023
+ textBefore,
1024
+ );
1025
+ if (isInExtArray) {
1026
+ const insert = `{\n${inner}"url": ""\n${indent}}`;
1027
+ return {
1028
+ changes: { from: braceFrom, to: braceTo, insert },
1029
+ selection: { anchor: braceFrom + insert.lastIndexOf('""') + 1 },
1030
+ };
1031
+ }
1032
+
1033
+ return {
1034
+ changes: {
1035
+ from: braceFrom,
1036
+ to: braceTo,
1037
+ insert: `{\n${inner}\n${indent}}`,
1038
+ },
1039
+ selection: { anchor: braceFrom + 2 + inner.length },
1040
+ };
1041
+ });
1042
+ }
1043
+
731
1044
  type CodeEditorProps = {
732
1045
  readOnly?: boolean;
733
1046
  isReadOnlyTheme?: boolean;
@@ -741,13 +1054,28 @@ type CodeEditorProps = {
741
1054
  additionalExtensions?: Extension[];
742
1055
  issueLineNumbers?: { line: number; message?: string }[];
743
1056
  foldGutter?: boolean;
744
- lintGutter?: boolean;
745
1057
  lineNumbers?: boolean;
746
- sqlExtraBuiltins?: string[];
1058
+ sql?: SqlConfig;
1059
+ getStructureDefinitions?: GetStructureDefinitions;
1060
+ resourceTypeHint?: string;
1061
+ expandValueSet?: ExpandValueSet;
1062
+ getUrlSuggestions?: GetUrlSuggestions;
1063
+ vimMode?: boolean;
747
1064
  };
748
1065
 
749
1066
  export type CodeEditorView = EditorView;
750
1067
 
1068
+ export type {
1069
+ ExpandValueSet,
1070
+ GetStructureDefinitions,
1071
+ } from "./fhir-autocomplete";
1072
+ export type { GetUrlSuggestions } from "./http";
1073
+ export type {
1074
+ SqlConfig,
1075
+ SqlMetadata,
1076
+ SqlQueryType,
1077
+ } from "./sql-completion";
1078
+
751
1079
  export function CodeEditor({
752
1080
  defaultValue,
753
1081
  currentValue,
@@ -761,13 +1089,28 @@ export function CodeEditor({
761
1089
  additionalExtensions,
762
1090
  issueLineNumbers,
763
1091
  foldGutter: enableFoldGutter = true,
764
- lintGutter: enableLintGutter = true,
765
1092
  lineNumbers: enableLineNumbers = true,
766
- sqlExtraBuiltins,
1093
+ sql,
1094
+ getStructureDefinitions,
1095
+ resourceTypeHint,
1096
+ expandValueSet,
1097
+ getUrlSuggestions,
1098
+ vimMode = false,
767
1099
  }: CodeEditorProps) {
768
1100
  const domRef = React.useRef(null);
769
1101
  const [view, setView] = React.useState<EditorView | null>(null);
770
1102
 
1103
+ const safeDispatch = React.useCallback(
1104
+ (spec: Parameters<EditorView["dispatch"]>[0]) => {
1105
+ try {
1106
+ view?.dispatch(spec);
1107
+ } catch {
1108
+ // Ignore RangeError from stale decoration positions during reconfigure
1109
+ }
1110
+ },
1111
+ [view],
1112
+ );
1113
+
771
1114
  const initialValue = React.useRef(defaultValue ?? "");
772
1115
 
773
1116
  const onChangeComparment = React.useRef(new Compartment());
@@ -776,6 +1119,13 @@ export function CodeEditor({
776
1119
  const readOnlyCompartment = React.useRef(new Compartment());
777
1120
  const themeCompartment = React.useRef(new Compartment());
778
1121
  const additionalExtensionsCompartment = React.useRef(new Compartment());
1122
+ const sqlCompletionCompartment = React.useRef(new Compartment());
1123
+ const fhirCompletionCompartment = React.useRef(new Compartment());
1124
+ const vimCompartment = React.useRef(new Compartment());
1125
+ const [sqlFunctions, setSqlFunctions] = React.useState<
1126
+ string[] | undefined
1127
+ >();
1128
+ const executeSqlRef = React.useRef(sql?.executeSql);
779
1129
 
780
1130
  React.useEffect(() => {
781
1131
  if (!domRef.current) {
@@ -787,10 +1137,28 @@ export function CodeEditor({
787
1137
  state: EditorState.create({
788
1138
  doc: initialValue.current,
789
1139
  extensions: [
1140
+ vimCompartment.current.of(vimMode ? vim() : []),
790
1141
  EditorView.contentAttributes.of({ "data-gramm": "false" }),
791
1142
  readOnlyCompartment.current.of(EditorState.readOnly.of(false)),
792
1143
  ...(enableLineNumbers ? [lineNumbers()] : []),
793
- ...(enableFoldGutter ? [foldGutter()] : []),
1144
+ ...(enableFoldGutter
1145
+ ? [
1146
+ foldGutter({
1147
+ markerDOM: (open) => {
1148
+ const el = document.createElement("span");
1149
+ el.style.display = "flex";
1150
+ el.style.alignItems = "center";
1151
+ el.style.justifyContent = "center";
1152
+ el.style.width = "100%";
1153
+ el.style.height = "100%";
1154
+ el.innerHTML = open
1155
+ ? '<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>'
1156
+ : '<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>';
1157
+ return el;
1158
+ },
1159
+ }),
1160
+ ]
1161
+ : []),
794
1162
  highlightSpecialChars(),
795
1163
  history(),
796
1164
  drawSelection(),
@@ -803,6 +1171,7 @@ export function CodeEditor({
803
1171
  autocompletion({
804
1172
  icons: false,
805
1173
  maxRenderedOptions: 1000,
1174
+ defaultKeymap: false,
806
1175
  addToOptions: [{ render: renderCompletionIcon, position: 20 }],
807
1176
  optionClass: (_completion) =>
808
1177
  "!px-2 !py-1 rounded-md aria-selected:!bg-bg-quaternary aria-selected:!text-text-primary hover:!bg-bg-secondary flex items-center gap-2",
@@ -816,27 +1185,58 @@ export function CodeEditor({
816
1185
  }),
817
1186
  rectangularSelection(),
818
1187
  crosshairCursor(),
819
- highlightActiveLine(),
820
- highlightActiveLineGutter(),
821
1188
  highlightSelectionMatches(),
1189
+ Prec.highest(
1190
+ keymap.of([
1191
+ {
1192
+ key: "Tab",
1193
+ run: (v) => {
1194
+ if (completionStatus(v.state) === "active") {
1195
+ return moveCompletionSelection(true)(v);
1196
+ }
1197
+ return false;
1198
+ },
1199
+ },
1200
+ {
1201
+ key: "Shift-Tab",
1202
+ run: (v) => {
1203
+ if (completionStatus(v.state) === "active") {
1204
+ return moveCompletionSelection(false)(v);
1205
+ }
1206
+ return false;
1207
+ },
1208
+ },
1209
+ {
1210
+ key: "Enter",
1211
+ run: (v) => {
1212
+ if (completionStatus(v.state) === "active") {
1213
+ return acceptCompletion(v);
1214
+ }
1215
+ return false;
1216
+ },
1217
+ },
1218
+ ]),
1219
+ ),
822
1220
  themeCompartment.current.of(baseTheme),
823
1221
  completionTheme,
824
1222
  keymap.of([
825
1223
  ...closeBracketsKeymap,
1224
+ ...completionKeymap.filter((b) => b.key !== "Enter"),
826
1225
  ...defaultKeymap,
827
1226
  ...searchKeymap,
828
1227
  ...historyKeymap,
829
1228
  ...foldKeymap,
830
- ...completionKeymap,
831
1229
  ...lintKeymap,
832
1230
  ]),
833
- ...(enableLintGutter ? [lintGutter()] : []),
834
1231
  issueLinesField,
835
1232
  errorTooltipHandler,
1233
+ EditorView.exceptionSink.of(() => {}),
836
1234
  ...customSearchExtension,
837
1235
  onChangeComparment.current.of([]),
838
1236
  onUpdateComparment.current.of([]),
839
1237
  additionalExtensionsCompartment.current.of([]),
1238
+ sqlCompletionCompartment.current.of([]),
1239
+ fhirCompletionCompartment.current.of([]),
840
1240
  ],
841
1241
  }),
842
1242
  });
@@ -847,7 +1247,69 @@ export function CodeEditor({
847
1247
  view.destroy();
848
1248
  setView(() => null);
849
1249
  };
850
- }, [enableFoldGutter, enableLineNumbers, enableLintGutter]);
1250
+ }, [enableFoldGutter, enableLineNumbers, vimMode]);
1251
+
1252
+ React.useEffect(() => {
1253
+ executeSqlRef.current = sql?.executeSql;
1254
+ });
1255
+
1256
+ React.useEffect(() => {
1257
+ if (!view || !sql) {
1258
+ if (view) {
1259
+ safeDispatch({
1260
+ effects: sqlCompletionCompartment.current.reconfigure([]),
1261
+ });
1262
+ }
1263
+ setSqlFunctions(undefined);
1264
+ return;
1265
+ }
1266
+
1267
+ let cancelled = false;
1268
+
1269
+ fetchSqlMetadata(sql.executeSql)
1270
+ .then((metadata) => {
1271
+ if (cancelled) return;
1272
+ setSqlFunctions(metadata.functions);
1273
+ const extensions = buildSqlCompletionExtensions(
1274
+ metadata,
1275
+ (query, type) =>
1276
+ executeSqlRef.current?.(query, type) ?? Promise.resolve([]),
1277
+ );
1278
+ safeDispatch({
1279
+ effects: sqlCompletionCompartment.current.reconfigure(extensions),
1280
+ });
1281
+ })
1282
+ .catch(() => {});
1283
+
1284
+ return () => {
1285
+ cancelled = true;
1286
+ };
1287
+ }, [view, sql, safeDispatch]);
1288
+
1289
+ React.useEffect(() => {
1290
+ if (!view) return;
1291
+ if (getStructureDefinitions) {
1292
+ safeDispatch({
1293
+ effects: fhirCompletionCompartment.current.reconfigure(
1294
+ buildFhirCompletionExtension(
1295
+ getStructureDefinitions,
1296
+ resourceTypeHint,
1297
+ expandValueSet,
1298
+ ),
1299
+ ),
1300
+ });
1301
+ } else {
1302
+ safeDispatch({
1303
+ effects: fhirCompletionCompartment.current.reconfigure([]),
1304
+ });
1305
+ }
1306
+ }, [
1307
+ view,
1308
+ getStructureDefinitions,
1309
+ resourceTypeHint,
1310
+ expandValueSet,
1311
+ safeDispatch,
1312
+ ]);
851
1313
 
852
1314
  React.useEffect(() => {
853
1315
  if (viewCallback && view) {
@@ -856,7 +1318,7 @@ export function CodeEditor({
856
1318
  }, [view, viewCallback]);
857
1319
 
858
1320
  React.useEffect(() => {
859
- view?.dispatch({
1321
+ safeDispatch({
860
1322
  effects: onChangeComparment.current.reconfigure([
861
1323
  EditorView.updateListener.of((update) => {
862
1324
  if (update.docChanged && onChange) {
@@ -865,10 +1327,10 @@ export function CodeEditor({
865
1327
  }),
866
1328
  ]),
867
1329
  });
868
- }, [view, onChange]);
1330
+ }, [onChange, safeDispatch]);
869
1331
 
870
1332
  React.useEffect(() => {
871
- view?.dispatch({
1333
+ safeDispatch({
872
1334
  effects: onUpdateComparment.current.reconfigure([
873
1335
  EditorView.updateListener.of((update) => {
874
1336
  if (onUpdate) {
@@ -877,7 +1339,7 @@ export function CodeEditor({
877
1339
  }),
878
1340
  ]),
879
1341
  });
880
- }, [view, onUpdate]);
1342
+ }, [onUpdate, safeDispatch]);
881
1343
 
882
1344
  // FIXME: it is probably better to have CM manage its state.
883
1345
  React.useEffect(() => {
@@ -887,7 +1349,7 @@ export function CodeEditor({
887
1349
 
888
1350
  const currentDoc = view.state.doc.toString();
889
1351
  if (currentDoc !== currentValue) {
890
- view.dispatch({
1352
+ safeDispatch({
891
1353
  changes: {
892
1354
  from: 0,
893
1355
  to: currentDoc.length,
@@ -895,66 +1357,84 @@ export function CodeEditor({
895
1357
  },
896
1358
  });
897
1359
  }
898
- }, [currentValue, view]);
1360
+ }, [currentValue, view, safeDispatch]);
1361
+
1362
+ const getUrlSuggestionsRef = React.useRef(getUrlSuggestions);
1363
+ getUrlSuggestionsRef.current = getUrlSuggestions;
1364
+
1365
+ const stableGetUrlSuggestions = React.useMemo(() => {
1366
+ if (!getUrlSuggestions) return undefined;
1367
+ return ((path: string, method: string) =>
1368
+ getUrlSuggestionsRef.current?.(path, method) ?? []) as GetUrlSuggestions;
1369
+ }, [getUrlSuggestions]);
899
1370
 
900
1371
  React.useEffect(() => {
901
1372
  if (view === null) {
902
1373
  return;
903
1374
  }
904
- view.dispatch({
1375
+ safeDispatch({
905
1376
  effects: languageCompartment.current.reconfigure(
906
- languageExtensions(mode, sqlExtraBuiltins),
1377
+ languageExtensions(mode, sqlFunctions, stableGetUrlSuggestions),
907
1378
  ),
908
1379
  });
909
- }, [mode, view, sqlExtraBuiltins]);
1380
+ }, [mode, view, sqlFunctions, stableGetUrlSuggestions, safeDispatch]);
910
1381
 
911
1382
  React.useEffect(() => {
912
1383
  if (view === null) {
913
1384
  return;
914
1385
  }
915
- view.dispatch({
1386
+ safeDispatch({
916
1387
  effects: [
917
1388
  readOnlyCompartment.current.reconfigure(
918
1389
  EditorState.readOnly.of(readOnly),
919
1390
  ),
920
1391
  ],
921
1392
  });
922
- }, [readOnly, view]);
1393
+ }, [readOnly, view, safeDispatch]);
923
1394
 
924
1395
  React.useEffect(() => {
925
1396
  if (view === null) {
926
1397
  return;
927
1398
  }
928
- view.dispatch({
1399
+ safeDispatch({
929
1400
  effects: [
930
1401
  themeCompartment.current.reconfigure(
931
1402
  isReadOnlyTheme ? readOnlyTheme : baseTheme,
932
1403
  ),
933
1404
  ],
934
1405
  });
935
- }, [isReadOnlyTheme, view]);
1406
+ }, [isReadOnlyTheme, view, safeDispatch]);
936
1407
 
937
1408
  React.useEffect(() => {
938
1409
  if (view === null) {
939
1410
  return;
940
1411
  }
941
- view.dispatch({
1412
+ safeDispatch({
1413
+ effects: [vimCompartment.current.reconfigure(vimMode ? vim() : [])],
1414
+ });
1415
+ }, [vimMode, view, safeDispatch]);
1416
+
1417
+ React.useEffect(() => {
1418
+ if (view === null) {
1419
+ return;
1420
+ }
1421
+ safeDispatch({
942
1422
  effects: [
943
1423
  additionalExtensionsCompartment.current.reconfigure(
944
1424
  additionalExtensions ?? [],
945
1425
  ),
946
1426
  ],
947
1427
  });
948
- }, [additionalExtensions, view]);
1428
+ }, [additionalExtensions, view, safeDispatch]);
949
1429
 
950
1430
  React.useEffect(() => {
951
1431
  if (view === null) {
952
1432
  return;
953
1433
  }
954
- view.dispatch({
1434
+ safeDispatch({
955
1435
  effects: setIssueLinesEffect.of(issueLineNumbers ?? []),
956
1436
  });
957
- }, [issueLineNumbers, view]);
1437
+ }, [issueLineNumbers, view, safeDispatch]);
958
1438
 
959
1439
  return <div className="h-full w-full" ref={domRef} id={id} />;
960
1440
  }
@@ -1012,21 +1492,30 @@ const editorInputTheme = EditorView.theme({
1012
1492
  });
1013
1493
 
1014
1494
  const KeywordIcon = () => <Terminal size={16} color="#717684" />;
1015
- const OperatorIcon = () => <Braces size={16} color="#717684" />;
1495
+ const OperatorIcon = () => <ChevronsRight size={16} color="#717684" />;
1496
+ const TableIcon = () => <Table2 size={16} color="#717684" />;
1497
+ const HeaderIcon = () => <Heading size={16} color="#717684" />;
1016
1498
 
1017
1499
  function getCompletionIcon(completion: Completion): React.FC | null {
1018
1500
  if (completion.type === "function") return SquareFunctionIcon;
1019
1501
  if (completion.type === "keyword") return KeywordIcon;
1020
1502
  if (completion.type === "operator") return OperatorIcon;
1503
+ if (completion.type === "table") return TableIcon;
1504
+ if (completion.type === "header") return HeaderIcon;
1505
+ if (completion.type === "text") return TypCodeIcon;
1506
+ if (completion.type === "type") return ResourceIcon;
1507
+ if (completion.type === "search-param") return null;
1021
1508
  const detail = completion.detail;
1022
1509
  if (!detail) {
1023
1510
  if (completion.type === "variable") return SquareFunctionIcon;
1024
- return null;
1511
+ return TypCodeIcon;
1025
1512
  }
1026
1513
  const typeName = detail.replace(/\[\]$/, "");
1027
- if (!typeName) return null;
1514
+ if (!typeName) return TypCodeIcon;
1515
+ // Search param types (TOKEN, REFERENCE) — no icon
1516
+ if (typeName === typeName.toUpperCase()) return null;
1028
1517
  const firstChar = typeName[0];
1029
- if (!firstChar) return null;
1518
+ if (!firstChar) return TypCodeIcon;
1030
1519
  const isComplex = firstChar === firstChar.toUpperCase();
1031
1520
  return isComplex ? ComplexTypeIcon : TypCodeIcon;
1032
1521
  }
@@ -1039,6 +1528,8 @@ function renderCompletionIcon(completion: Completion): Node {
1039
1528
  flushSync(() => {
1040
1529
  createRoot(container).render(<Icon />);
1041
1530
  });
1531
+ } else {
1532
+ container.style.display = "none";
1042
1533
  }
1043
1534
  return container;
1044
1535
  }