@keboola/design 8.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/rjsf.js CHANGED
@@ -1,11 +1,11 @@
1
- import React26, { createContext, use, useCallback, useMemo, useRef, useState, useEffect, useContext, isValidElement, useId, useLayoutEffect, useSyncExternalStore, Children, Fragment as Fragment$1 } from 'react';
1
+ import React26, { createContext, use, useCallback, useMemo, useRef, useState, useEffect, useContext, isValidElement, useLayoutEffect, useId, useSyncExternalStore, Children, Fragment as Fragment$1 } from 'react';
2
2
  import Form2 from '@rjsf/core';
3
3
  import validator from '@rjsf/validator-ajv8';
4
4
  import { completeAnyWord, autocompletion } from '@codemirror/autocomplete';
5
5
  import { jsonParseLinter, json } from '@codemirror/lang-json';
6
6
  import { linter } from '@codemirror/lint';
7
7
  import { unifiedMergeView } from '@codemirror/merge';
8
- import { EditorView, keymap, placeholder } from '@codemirror/view';
8
+ import { EditorView, Decoration, ViewPlugin, keymap, placeholder, tooltips } from '@codemirror/view';
9
9
  import CodeMirror, { EditorState } from '@uiw/react-codemirror';
10
10
  import { clsx } from 'clsx';
11
11
  import { extendTailwindMerge } from 'tailwind-merge';
@@ -23,9 +23,10 @@ import { r } from '@codemirror/legacy-modes/mode/r';
23
23
  import { shell } from '@codemirror/legacy-modes/mode/shell';
24
24
  import { toml } from '@codemirror/legacy-modes/mode/toml';
25
25
  import { githubLightInit } from '@uiw/codemirror-theme-github';
26
- import { sfsql } from '@keboola/codemirror-lang-sfsql';
26
+ import { getSqlDialect } from '@keboola/codemirror-lang-sql';
27
27
  import colors from '@keboola/tailwind-config/colors';
28
28
  import { simpleMode } from '@codemirror/legacy-modes/mode/simple-mode';
29
+ import { StateEffect, StateField, Compartment } from '@codemirror/state';
29
30
  import DOMPurify from 'dompurify';
30
31
  import { Root as Root$2, CollapsibleTrigger, Content as Content$1 } from '@radix-ui/react-collapsible';
31
32
  import * as RadixTabs from '@radix-ui/react-tabs';
@@ -35,6 +36,7 @@ import TextareaAutosize from 'react-textarea-autosize';
35
36
  import { Root, Indicator } from '@radix-ui/react-checkbox';
36
37
  import { Root as Root$3, Trigger as Trigger$1, Close, Portal as Portal$1, Content as Content$2 } from '@radix-ui/react-popover';
37
38
  import { Combobox as Combobox$1 } from '@base-ui/react/combobox';
39
+ import { useVirtualizer } from '@tanstack/react-virtual';
38
40
  import * as RRadioGroup from '@radix-ui/react-radio-group';
39
41
  import { Item, Indicator as Indicator$1 } from '@radix-ui/react-radio-group';
40
42
  import * as RSeparator from '@radix-ui/react-separator';
@@ -352,6 +354,9 @@ Tooltip.Trigger = TooltipTrigger;
352
354
  // src/utils/createPrefixedString.ts
353
355
  var createPrefixedString = (prefix2) => (literals) => literals.reduce((acc, literal) => acc + literal, `${prefix2}-`);
354
356
 
357
+ // src/utils/escapeStringRegexp.ts
358
+ var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
359
+
355
360
  // src/utils/mergeRefs.ts
356
361
  function setRef(ref, value) {
357
362
  if (typeof ref === "function") {
@@ -363,6 +368,21 @@ function setRef(ref, value) {
363
368
  var mergeRefs = (...refs) => {
364
369
  return (node) => refs.forEach((ref) => setRef(ref, node));
365
370
  };
371
+
372
+ // src/utils/isMac.ts
373
+ var getIsMac = () => {
374
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
375
+ const platform = navigator.userAgentData.platform;
376
+ if (typeof platform === "string") {
377
+ return platform.toLowerCase() === "macos";
378
+ }
379
+ }
380
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
381
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
382
+ }
383
+ return false;
384
+ };
385
+ var isMac = getIsMac();
366
386
  var helpVariants = cva(["tw:m-0", "tw:text-xs"], {
367
387
  defaultVariants: {
368
388
  state: "default"
@@ -813,8 +833,6 @@ var ButtonToolbar = ({ value, children }) => {
813
833
  };
814
834
  var COMMANDS = [
815
835
  "run-operation",
816
- "source freshness",
817
- "docs generate",
818
836
  "run",
819
837
  "test",
820
838
  "seed",
@@ -823,9 +841,13 @@ var COMMANDS = [
823
841
  "ls",
824
842
  "compile",
825
843
  "build",
826
- "deps"
844
+ "deps",
845
+ "docs",
846
+ "source"
827
847
  ];
848
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
828
849
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
850
+ var SELECTORS = ["--select", "--exclude", "-s"];
829
851
  var METHODS = [
830
852
  "path:",
831
853
  "source:",
@@ -852,13 +874,59 @@ var FLAGS = [
852
874
  "--profile",
853
875
  "--full-refresh"
854
876
  ];
855
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
856
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
877
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
878
+ const subcommands = SUBCOMMANDS[command];
879
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
880
+ });
881
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
882
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
883
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
884
+ var WORD = /[\w:-]*$/;
885
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
886
+ var DBT = [{ label: "dbt", type: "keyword" }];
887
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
888
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
889
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
890
+ command,
891
+ asOptions(subcommands, "keyword")
892
+ ])
893
+ );
894
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
895
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
896
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
897
+ var optionsAfter = (words) => {
898
+ const [step, command, ...rest] = words;
899
+ if (step === void 0) return DBT;
900
+ if (step.toLowerCase() !== "dbt") return [];
901
+ if (command === void 0) return COMMAND_OPTIONS;
902
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
903
+ if (subcommands && rest.length === 0) return subcommands;
904
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
905
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
906
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
907
+ };
908
+ var complete = (context) => {
909
+ const typed = context.matchBefore(WORD);
910
+ if (!typed?.text && !context.explicit) return null;
911
+ const from = typed?.from ?? context.pos;
912
+ const line = context.state.doc.lineAt(from);
913
+ const options = optionsAfter(
914
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
915
+ );
916
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
917
+ };
857
918
  var dbt = simpleMode({
919
+ languageData: {
920
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
921
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
922
+ autocomplete: complete
923
+ },
858
924
  start: [
859
925
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
860
- { regex: /dbt/, token: "keyword", sol: true },
861
- { regex: word(COMMANDS), token: "keyword" },
926
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
927
+ // colour — the same thing the completion source asks of it.
928
+ { regex: /dbt\b/, token: "keyword", sol: true },
929
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
862
930
  { regex: word(ARGUMENTS), token: "variableName" },
863
931
  { regex: prefix(METHODS), token: "propertyName" },
864
932
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -906,6 +974,8 @@ var KEYWORDS = {
906
974
  };
907
975
  var jsonnet = {
908
976
  name: "jsonnet",
977
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
978
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
909
979
  startState: () => ({
910
980
  cComment: false,
911
981
  textBlock: false,
@@ -1038,6 +1108,7 @@ var LANGUAGE_MODES = [
1038
1108
  "text/x-mysql",
1039
1109
  "text/x-pgsql",
1040
1110
  "text/x-mssql",
1111
+ // Snowflake, under a historical name — these strings are a published prop contract.
1041
1112
  "text/x-sfsql",
1042
1113
  "text/x-plsql",
1043
1114
  "text/x-rsrc",
@@ -1099,8 +1170,6 @@ var ALWAYS_OFF = {
1099
1170
  var buildBasicSetup = (readOnly) => ({
1100
1171
  ...ALWAYS_OFF,
1101
1172
  ...readOnly ? {
1102
- history: false,
1103
- historyKeymap: false,
1104
1173
  closeBrackets: false,
1105
1174
  closeBracketsKeymap: false,
1106
1175
  lintKeymap: false,
@@ -1138,7 +1207,7 @@ var getLanguageMode = (mode) => {
1138
1207
  case "text/x-mssql":
1139
1208
  return sql({ upperCaseKeywords: true, dialect: MSSQL });
1140
1209
  case "text/x-sfsql":
1141
- return StreamLanguage.define(sfsql);
1210
+ return sql({ upperCaseKeywords: true, dialect: getSqlDialect("snowflake") });
1142
1211
  case "text/x-plsql":
1143
1212
  return sql({ upperCaseKeywords: true, dialect: PLSQL });
1144
1213
  case "text/x-rsrc":
@@ -1163,6 +1232,112 @@ var getLanguageMode = (mode) => {
1163
1232
  }
1164
1233
  };
1165
1234
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1235
+ var setSearchEffect = StateEffect.define();
1236
+ function findMatches(doc, query) {
1237
+ if (!query) return [];
1238
+ const text = doc.toString();
1239
+ try {
1240
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1241
+ return Array.from(text.matchAll(regex)).map((match2, index) => {
1242
+ const from = match2.index;
1243
+ const to = from + match2[0].length;
1244
+ return { from, to, text: match2[0], localIndex: index };
1245
+ });
1246
+ } catch (e) {
1247
+ console.warn("Invalid search regex:", e);
1248
+ return [];
1249
+ }
1250
+ }
1251
+ function createDecorations(matches, selectedLocalIndex) {
1252
+ const decorations = matches.map((match2) => {
1253
+ const isSelected = match2.localIndex === selectedLocalIndex;
1254
+ const decoration = Decoration.mark({
1255
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1256
+ });
1257
+ return decoration.range(match2.from, match2.to);
1258
+ });
1259
+ return Decoration.set(decorations, true);
1260
+ }
1261
+ var searchField = StateField.define({
1262
+ create() {
1263
+ return {
1264
+ query: "",
1265
+ selectedMatchIndex: void 0,
1266
+ matches: [],
1267
+ decorations: Decoration.none
1268
+ };
1269
+ },
1270
+ update(state, tr) {
1271
+ let newState = state;
1272
+ for (const effect of tr.effects) {
1273
+ if (!effect.is(setSearchEffect)) {
1274
+ continue;
1275
+ }
1276
+ const { query, selectedMatchIndex } = effect.value;
1277
+ const matches = findMatches(tr.state.doc, query);
1278
+ const decorations = createDecorations(matches, selectedMatchIndex);
1279
+ newState = {
1280
+ query,
1281
+ selectedMatchIndex,
1282
+ matches,
1283
+ decorations
1284
+ };
1285
+ }
1286
+ if (tr.docChanged && newState.query) {
1287
+ const matches = findMatches(tr.state.doc, newState.query);
1288
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1289
+ newState = {
1290
+ ...newState,
1291
+ matches,
1292
+ decorations
1293
+ };
1294
+ }
1295
+ return newState;
1296
+ },
1297
+ provide: (field) => EditorView.decorations.from(field, (state) => state.decorations)
1298
+ });
1299
+ var searchViewPlugin = ViewPlugin.fromClass(
1300
+ class {
1301
+ lastSelectedIndex;
1302
+ update(update) {
1303
+ const state = update.state.field(searchField);
1304
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1305
+ const match2 = state.matches[state.selectedMatchIndex];
1306
+ if (match2) {
1307
+ requestAnimationFrame(() => {
1308
+ update.view.dispatch({
1309
+ effects: EditorView.scrollIntoView(match2.from, { y: "center" })
1310
+ });
1311
+ });
1312
+ }
1313
+ this.lastSelectedIndex = state.selectedMatchIndex;
1314
+ }
1315
+ if (state.selectedMatchIndex === void 0) {
1316
+ this.lastSelectedIndex = void 0;
1317
+ }
1318
+ }
1319
+ }
1320
+ );
1321
+ var searchTheme = EditorView.baseTheme({
1322
+ ".cm-searchMatch": {
1323
+ backgroundColor: colors.blue[100]
1324
+ },
1325
+ ".cm-searchMatch > span": {
1326
+ color: `${colors.blue[600]} !important`
1327
+ },
1328
+ ".cm-searchMatch-selected": {
1329
+ backgroundColor: colors.blue[600]
1330
+ },
1331
+ ".cm-searchMatch-selected > span": {
1332
+ color: `${colors.white} !important`
1333
+ }
1334
+ });
1335
+ function searchExtension() {
1336
+ return [searchField, searchViewPlugin, searchTheme];
1337
+ }
1338
+ function updateSearch(view, query, selectedMatchIndex) {
1339
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1340
+ }
1166
1341
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1167
1342
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1168
1343
  return /* @__PURE__ */ jsx(
@@ -1222,12 +1397,79 @@ Kbd.Group = KbdGroup;
1222
1397
  Kbd.Separator = KbdSeparator;
1223
1398
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxs("div", { children: [
1224
1399
  "Use ",
1225
- /* @__PURE__ */ jsx(Kbd, { children: "Ctrl+/" }),
1226
- " or ",
1227
- /* @__PURE__ */ jsx(Kbd, { children: "Command+/" }),
1400
+ /* @__PURE__ */ jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1228
1401
  " to toggle comments."
1229
1402
  ] });
1230
- var emptyArray = [];
1403
+ var establishesFixedContainingBlock = (style) => style.transform !== "none" || style.translate !== "none" || style.rotate !== "none" || style.scale !== "none" || style.perspective !== "none" || style.filter !== "none" || style.backdropFilter !== "none" || /\b(paint|layout|strict|content)\b/.test(style.contain) || /\b(transform|perspective|filter|contain)\b/.test(style.willChange);
1404
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1405
+ var findTooltipHost = (from) => {
1406
+ let outermostCropper = null;
1407
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1408
+ const style = getComputedStyle(node);
1409
+ if (crops(style)) {
1410
+ outermostCropper = node;
1411
+ } else if (establishesFixedContainingBlock(style)) {
1412
+ return outermostCropper?.parentElement ?? node;
1413
+ }
1414
+ }
1415
+ return outermostCropper?.parentElement ?? null;
1416
+ };
1417
+ var tooltipsExtension = () => {
1418
+ const layer = new Compartment();
1419
+ return [
1420
+ layer.of(tooltips()),
1421
+ ViewPlugin.define((view) => {
1422
+ const scheduled = setTimeout(() => {
1423
+ if (!view.dom.isConnected) return;
1424
+ const parent = findTooltipHost(view.dom);
1425
+ if (parent) view.dispatch({ effects: layer.reconfigure(tooltips({ parent })) });
1426
+ });
1427
+ return { destroy: () => clearTimeout(scheduled) };
1428
+ })
1429
+ ];
1430
+ };
1431
+ var useTooltipsExtension = () => useMemo(tooltipsExtension, []);
1432
+
1433
+ // src/components/CodeEditor/variableCompletion.ts
1434
+ var BOOST = 2e3;
1435
+ var NAME = String.raw`[\w-]*`;
1436
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1437
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1438
+ var OPENING = /\{{1,2} ?$/;
1439
+ var CLOSING = /^ ?\}{1,2}/;
1440
+ var completeVariables = (variables = []) => {
1441
+ if (variables.length === 0) return () => null;
1442
+ const options = variables.map(({ name, value }) => ({
1443
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1444
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1445
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1446
+ label: name,
1447
+ displayLabel: `{{${name}}}`,
1448
+ detail: value,
1449
+ type: "variable",
1450
+ boost: BOOST,
1451
+ apply: (view, completion, from, to) => {
1452
+ const { doc } = view.state;
1453
+ const line = doc.lineAt(from);
1454
+ const insert = `{{${completion.label}}}`;
1455
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1456
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1457
+ const start = opening ? from - opening[0].length : from;
1458
+ const end = closing ? to + closing[0].length : to;
1459
+ view.dispatch({
1460
+ changes: { from: start, to: end, insert },
1461
+ selection: { anchor: start + insert.length }
1462
+ });
1463
+ }
1464
+ }));
1465
+ return (context) => {
1466
+ const match2 = context.matchBefore(AT_CARET);
1467
+ if (!match2?.text) return null;
1468
+ const [name = ""] = NAME_AT_END.exec(match2.text) ?? [];
1469
+ return { from: match2.to - name.length, options };
1470
+ };
1471
+ };
1472
+ var useVariableCompletion = (variables = []) => useStableCallback(completeVariables(variables));
1231
1473
  var emptyObject = {};
1232
1474
  var CodeEditor = ({
1233
1475
  value,
@@ -1236,8 +1478,13 @@ var CodeEditor = ({
1236
1478
  mode = "application/json",
1237
1479
  placeholder: placeholder$1 = "",
1238
1480
  ariaLabel,
1481
+ searchQuery,
1482
+ searchMatchIndex,
1239
1483
  readOnly = false,
1240
- autocompletions = emptyArray,
1484
+ inputLocked = false,
1485
+ autocompletions,
1486
+ completionSource,
1487
+ variables,
1241
1488
  extraKeys = emptyObject,
1242
1489
  withToggleCommentHint = false,
1243
1490
  withAutofocus = false,
@@ -1252,49 +1499,86 @@ var CodeEditor = ({
1252
1499
  className
1253
1500
  }) => {
1254
1501
  const handleChange = useStableCallback((next) => onChange?.(next));
1502
+ const withSearch = searchQuery !== void 0;
1503
+ const editorRef = useRef(null);
1504
+ useEffect(() => {
1505
+ const view = editorRef.current?.view;
1506
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1507
+ }, [withSearch, searchQuery, searchMatchIndex]);
1508
+ const handleCreateEditor = useStableCallback((view) => {
1509
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1510
+ });
1511
+ const extraKeysRef = useRef(extraKeys);
1512
+ useLayoutEffect(() => {
1513
+ extraKeysRef.current = extraKeys;
1514
+ });
1515
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1516
+ const boundKeys = useMemo(
1517
+ () => boundKeysSignature.split("\n").filter(Boolean),
1518
+ [boundKeysSignature]
1519
+ );
1520
+ const tooltipLayer = useTooltipsExtension();
1521
+ const variableCompletion = useVariableCompletion(variables);
1255
1522
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1256
1523
  const baseTheme = useThemedExtension(
1257
1524
  "code-editor:chrome",
1258
1525
  () => EditorView.theme(buildBaseThemeStyles())
1259
1526
  );
1260
1527
  const commonExtensions = useMemo(() => {
1528
+ const callerCompletions = completionSource ?? (completeAny ? completeAnyWord : autocompletions?.length ? autocompletions : null);
1529
+ const completionEntries = [
1530
+ ...callerCompletions ? [{ autocomplete: callerCompletions }] : [],
1531
+ { autocomplete: variableCompletion }
1532
+ ];
1261
1533
  return [
1262
1534
  getLanguageMode(mode),
1263
1535
  shouldLintJson(mode, readOnly) ? linter(jsonParseLinter()) : [],
1264
1536
  withLineWrapping ? EditorView.lineWrapping : [],
1265
- EditorState.languageData.of(() => [
1266
- { autocomplete: completeAny ? completeAnyWord : autocompletions }
1267
- ]),
1537
+ EditorState.languageData.of(() => completionEntries),
1268
1538
  keymap.of(
1269
- Object.entries(extraKeys).map(([key, run]) => ({
1539
+ boundKeys.map((key) => ({
1270
1540
  key,
1271
1541
  run: () => {
1272
- if (typeof run === "string") return run;
1273
- run();
1542
+ extraKeysRef.current[key]?.();
1274
1543
  return true;
1275
1544
  }
1276
1545
  }))
1277
1546
  ),
1278
- readOnly ? [] : autocompletion({ activateOnTyping: completeAny }),
1547
+ readOnly ? [] : [
1548
+ // CodeMirror's own default. Gating it on the props hid completions the
1549
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1550
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1551
+ // popup opens anyway.
1552
+ autocompletion()
1553
+ ],
1279
1554
  ariaLabel ? EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1555
+ withSearch ? searchExtension() : [],
1556
+ tooltipLayer,
1280
1557
  baseTheme
1281
1558
  ];
1282
1559
  }, [
1283
1560
  autocompletions,
1561
+ completionSource,
1562
+ variableCompletion,
1284
1563
  completeAny,
1285
- extraKeys,
1564
+ boundKeys,
1286
1565
  mode,
1287
1566
  readOnly,
1288
1567
  withLineWrapping,
1289
1568
  baseTheme,
1290
- ariaLabel
1569
+ ariaLabel,
1570
+ withSearch,
1571
+ tooltipLayer
1291
1572
  ]);
1292
1573
  const extensions = useMemo(() => {
1293
1574
  return [
1294
1575
  ...commonExtensions,
1295
1576
  placeholder(placeholder$1),
1296
1577
  EditorView.editable.of(!readOnly),
1297
- EditorState.readOnly.of(readOnly),
1578
+ // The lock is this facet and nothing else. `contenteditable` is what the
1579
+ // read-only wash keys off, and `basicSetup` is what holds the undo history,
1580
+ // so leaving both alone is what makes the lock invisible.
1581
+ EditorState.readOnly.of(readOnly || inputLocked),
1298
1582
  previousValue ? unifiedMergeView({
1299
1583
  original: previousValue,
1300
1584
  mergeControls: false,
@@ -1302,13 +1586,16 @@ var CodeEditor = ({
1302
1586
  collapseUnchanged: withCollapseUnchanged ? {} : void 0
1303
1587
  }) : []
1304
1588
  ];
1305
- }, [commonExtensions, placeholder$1, readOnly, previousValue, withCollapseUnchanged]);
1589
+ }, [commonExtensions, placeholder$1, readOnly, inputLocked, previousValue, withCollapseUnchanged]);
1306
1590
  const config = useMemo(() => {
1307
1591
  return {
1308
1592
  ...buildBasicSetup(readOnly),
1309
- lineNumbers: withLineNumbers
1593
+ lineNumbers: withLineNumbers,
1594
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1595
+ // second match count over the same document beside the caller's input.
1596
+ ...withSearch ? { searchKeymap: false } : null
1310
1597
  };
1311
- }, [withLineNumbers, readOnly]);
1598
+ }, [withLineNumbers, readOnly, withSearch]);
1312
1599
  const additionalButtons = renderAdditionalButtons?.(value);
1313
1600
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1314
1601
  return (
@@ -1331,6 +1618,8 @@ var CodeEditor = ({
1331
1618
  /* @__PURE__ */ jsx(
1332
1619
  CodeMirror,
1333
1620
  {
1621
+ ref: editorRef,
1622
+ onCreateEditor: handleCreateEditor,
1334
1623
  theme: customTheme,
1335
1624
  value,
1336
1625
  onChange: handleChange,
@@ -8914,9 +9203,9 @@ var ComboboxItemIndicator = ({ ref, className, variant = "check", ...props }) =>
8914
9203
  "tw:absolute",
8915
9204
  "tw:right-2",
8916
9205
  "tw:group-data-[variant=default]/combobox-content:right-4",
8917
- // Lucide SVGs default to 24×24 — constrain the rendered child so the
8918
- // 16px indicator slot owns the actual size.
8919
- 'tw:[&_svg:not([class*="tw:size-"])]:size-4',
9206
+ // `render={<Check />}` makes the icon the indicator element itself (not a descendant), so a
9207
+ // `[&_svg]` rule never matches it — size it directly or Lucide renders at its 24px default.
9208
+ "tw:size-4",
8920
9209
  "tw:group-data-[highlighted]/combobox-item:text-accent-foreground",
8921
9210
  "tw:group-data-[variant=default]/combobox-content:group-data-[highlighted]/combobox-item:text-foreground",
8922
9211
  className
@@ -9041,6 +9330,9 @@ var ComboboxGroupLabel = ({ ref, className, ...props }) => /* @__PURE__ */ jsx(
9041
9330
  }
9042
9331
  );
9043
9332
  var ComboboxCollection = ({ ...props }) => /* @__PURE__ */ jsx(Combobox$1.Collection, { "data-slot": "combobox-collection", ...props });
9333
+ var VIRTUALIZE_THRESHOLD = 30;
9334
+ var ESTIMATED_ROW_HEIGHT = 40;
9335
+ var ESTIMATED_LIST_HEIGHT = 320;
9044
9336
  var Combobox = ({
9045
9337
  // root props
9046
9338
  items: _itemsProp,
@@ -9067,6 +9359,7 @@ var Combobox = ({
9067
9359
  empty = "No items",
9068
9360
  indicator,
9069
9361
  prefix: prefix2,
9362
+ virtualized = false,
9070
9363
  // creatable
9071
9364
  allowCreate,
9072
9365
  queryToCreateValue,
@@ -9090,7 +9383,9 @@ var Combobox = ({
9090
9383
  const label = value === null ? "" : itemToStringLabelProp?.(value) ?? toLabel(value);
9091
9384
  setInputValue((prev) => prev === label ? prev : label);
9092
9385
  }, [value]);
9093
- const query = inputValue.trim();
9386
+ const selectedLabel = value == null ? void 0 : itemToStringLabelProp?.(value) ?? toLabel(value);
9387
+ const isShowingSelection = selectedLabel !== void 0 && inputValue === selectedLabel;
9388
+ const query = (isShowingSelection ? "" : inputValue).trim();
9094
9389
  const queryLower = query.toLowerCase();
9095
9390
  const itemsProp = _itemsProp ?? [];
9096
9391
  const sampleItem = itemsProp.flatMap((item) => isGroupItem(item) ? item.items : [item])[0];
@@ -9115,7 +9410,7 @@ var Combobox = ({
9115
9410
  if (isCreatableItem(value2)) return value2.value;
9116
9411
  return itemToStringLabelProp?.(value2) ?? toLabel(value2);
9117
9412
  };
9118
- const wrappedFilter = !allowCreate || !filter ? filter : (item, ...args) => isCreatableItem(item) || filter(item, ...args);
9413
+ const wrappedFilter = isShowingSelection ? () => true : !allowCreate || !filter ? filter : (item, ...args) => isCreatableItem(item) || filter(item, ...args);
9119
9414
  const wrappedItemToStringValue = !allowCreate || !itemToStringValue ? itemToStringValue : (item) => isCreatableItem(item) ? item.value : itemToStringValue(item);
9120
9415
  const isItemEqualToValue = (a, b) => {
9121
9416
  if (isCreatableItem(a) || isCreatableItem(b)) return toValueKey(a) === toValueKey(b);
@@ -9150,10 +9445,34 @@ var Combobox = ({
9150
9445
  closeReasonRef.current = "item-press";
9151
9446
  onValueChange?.(next, event);
9152
9447
  };
9153
- const renderItem = (item) => {
9448
+ const [listElement, setListElement] = useState(null);
9449
+ const isGrouped = isGroupItems(allItems);
9450
+ const shouldVirtualize = !isGrouped && (virtualized === true || virtualized === "auto" && allItems.length > VIRTUALIZE_THRESHOLD);
9451
+ const matchesQuery = (item) => {
9452
+ if (isCreatableItem(item)) return true;
9453
+ if (!query) return true;
9454
+ if (filter) return !!filter(item, query, (value2) => itemToStringLabel(value2) ?? toLabel(value2));
9455
+ return (itemToStringLabel(item) ?? toLabel(item)).toLowerCase().includes(queryLower);
9456
+ };
9457
+ const virtualItems = shouldVirtualize ? items.filter(matchesQuery) : [];
9458
+ const virtualizer = useVirtualizer({
9459
+ count: virtualItems.length,
9460
+ getScrollElement: () => listElement,
9461
+ estimateSize: () => ESTIMATED_ROW_HEIGHT,
9462
+ overscan: 12,
9463
+ enabled: shouldVirtualize,
9464
+ // Seed a viewport height so rows paint on the first frame. Without it the list measures 0 until
9465
+ // the ResizeObserver fires (a frame later), which renders an empty popup on open.
9466
+ initialRect: { width: 0, height: ESTIMATED_LIST_HEIGHT }
9467
+ });
9468
+ const renderItem = (item, index) => {
9469
+ if (isCreatableItem(item)) {
9470
+ return /* @__PURE__ */ jsx(Combobox.Item, { index, value: item, icon: /* @__PURE__ */ jsx(Plus, {}), children: queryToCreateLabel(item.value) });
9471
+ }
9154
9472
  return /* @__PURE__ */ jsx(
9155
9473
  Combobox.Item,
9156
9474
  {
9475
+ index,
9157
9476
  value: item,
9158
9477
  indicator,
9159
9478
  icon: itemToIcon(item),
@@ -9162,13 +9481,20 @@ var Combobox = ({
9162
9481
  }
9163
9482
  );
9164
9483
  };
9484
+ const rowStyle = (offset) => ({
9485
+ position: "absolute",
9486
+ top: 0,
9487
+ left: 0,
9488
+ width: "100%",
9489
+ transform: `translateY(${offset}px)`
9490
+ });
9165
9491
  return /* @__PURE__ */ jsxs(
9166
9492
  Combobox$1.Root,
9167
9493
  {
9168
- items,
9494
+ items: shouldVirtualize ? virtualItems : items,
9169
9495
  value,
9170
9496
  defaultValue,
9171
- filter: wrappedFilter,
9497
+ filter: shouldVirtualize ? null : wrappedFilter,
9172
9498
  onValueChange: handleOnValueChange,
9173
9499
  itemToStringLabel,
9174
9500
  isItemEqualToValue,
@@ -9178,6 +9504,12 @@ var Combobox = ({
9178
9504
  inputRef,
9179
9505
  inputValue,
9180
9506
  onInputValueChange: handleInputValueChange,
9507
+ virtualized: shouldVirtualize,
9508
+ onItemHighlighted: (_, details) => {
9509
+ if (shouldVirtualize && details.reason === "keyboard") {
9510
+ virtualizer.scrollToIndex(details.index);
9511
+ }
9512
+ },
9181
9513
  children: [
9182
9514
  /* @__PURE__ */ jsxs(
9183
9515
  ComboboxInputGroup,
@@ -9199,7 +9531,30 @@ var Combobox = ({
9199
9531
  ),
9200
9532
  /* @__PURE__ */ jsxs(ComboboxContent, { children: [
9201
9533
  /* @__PURE__ */ jsx(ComboboxEmpty, { children: empty }),
9202
- /* @__PURE__ */ jsx(ComboboxList, { children: (itemOrGroup) => {
9534
+ shouldVirtualize ? /* @__PURE__ */ jsx(ComboboxList, { ref: setListElement, children: /* @__PURE__ */ jsx(
9535
+ "div",
9536
+ {
9537
+ style: {
9538
+ position: "relative",
9539
+ width: "100%",
9540
+ height: `${virtualizer.getTotalSize()}px`
9541
+ },
9542
+ children: virtualizer.getVirtualItems().map((row) => {
9543
+ const item = virtualItems[row.index];
9544
+ if (item === void 0) return null;
9545
+ return /* @__PURE__ */ jsx(
9546
+ "div",
9547
+ {
9548
+ "data-index": row.index,
9549
+ ref: virtualizer.measureElement,
9550
+ style: rowStyle(row.start),
9551
+ children: renderItem(item, row.index)
9552
+ },
9553
+ row.key
9554
+ );
9555
+ })
9556
+ }
9557
+ ) }) : /* @__PURE__ */ jsx(ComboboxList, { children: (itemOrGroup) => {
9203
9558
  if (isCreatableItem(itemOrGroup))
9204
9559
  return /* @__PURE__ */ jsx(Combobox.Item, { value: itemOrGroup, icon: /* @__PURE__ */ jsx(Plus, {}), children: queryToCreateLabel(itemOrGroup.value) });
9205
9560
  if (isGroupItem(itemOrGroup)) {
@@ -9210,7 +9565,7 @@ var Combobox = ({
9210
9565
  const groupLabel = groupToLabel(itemOrGroup);
9211
9566
  return /* @__PURE__ */ jsxs(Combobox.Group, { items: itemOrGroup.items, children: [
9212
9567
  groupLabel && /* @__PURE__ */ jsx(Combobox.GroupLabel, { children: groupLabel }),
9213
- /* @__PURE__ */ jsx(Combobox.GroupList, { children: renderItem })
9568
+ /* @__PURE__ */ jsx(Combobox.GroupList, { children: (item) => renderItem(item) })
9214
9569
  ] });
9215
9570
  }
9216
9571
  return renderItem(itemOrGroup);