@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.cjs CHANGED
@@ -25,9 +25,10 @@ var r = require('@codemirror/legacy-modes/mode/r');
25
25
  var shell = require('@codemirror/legacy-modes/mode/shell');
26
26
  var toml = require('@codemirror/legacy-modes/mode/toml');
27
27
  var codemirrorThemeGithub = require('@uiw/codemirror-theme-github');
28
- var codemirrorLangSfsql = require('@keboola/codemirror-lang-sfsql');
28
+ var codemirrorLangSql = require('@keboola/codemirror-lang-sql');
29
29
  var colors = require('@keboola/tailwind-config/colors');
30
30
  var simpleMode = require('@codemirror/legacy-modes/mode/simple-mode');
31
+ var state = require('@codemirror/state');
31
32
  var DOMPurify = require('dompurify');
32
33
  var reactCollapsible = require('@radix-ui/react-collapsible');
33
34
  var RadixTabs = require('@radix-ui/react-tabs');
@@ -37,6 +38,7 @@ var TextareaAutosize = require('react-textarea-autosize');
37
38
  var reactCheckbox = require('@radix-ui/react-checkbox');
38
39
  var reactPopover = require('@radix-ui/react-popover');
39
40
  var combobox = require('@base-ui/react/combobox');
41
+ var reactVirtual = require('@tanstack/react-virtual');
40
42
  var RRadioGroup = require('@radix-ui/react-radio-group');
41
43
  var RSeparator = require('@radix-ui/react-separator');
42
44
 
@@ -386,6 +388,9 @@ Tooltip.Trigger = TooltipTrigger;
386
388
  // src/utils/createPrefixedString.ts
387
389
  var createPrefixedString = (prefix2) => (literals) => literals.reduce((acc, literal) => acc + literal, `${prefix2}-`);
388
390
 
391
+ // src/utils/escapeStringRegexp.ts
392
+ var escapeStringRegexp = (string) => string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
393
+
389
394
  // src/utils/mergeRefs.ts
390
395
  function setRef(ref, value) {
391
396
  if (typeof ref === "function") {
@@ -397,6 +402,21 @@ function setRef(ref, value) {
397
402
  var mergeRefs = (...refs) => {
398
403
  return (node) => refs.forEach((ref) => setRef(ref, node));
399
404
  };
405
+
406
+ // src/utils/isMac.ts
407
+ var getIsMac = () => {
408
+ if (typeof navigator !== "undefined" && "userAgentData" in navigator) {
409
+ const platform = navigator.userAgentData.platform;
410
+ if (typeof platform === "string") {
411
+ return platform.toLowerCase() === "macos";
412
+ }
413
+ }
414
+ if (typeof navigator !== "undefined" && typeof navigator.platform === "string") {
415
+ return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
416
+ }
417
+ return false;
418
+ };
419
+ var isMac = getIsMac();
400
420
  var helpVariants = classVarianceAuthority.cva(["tw:m-0", "tw:text-xs"], {
401
421
  defaultVariants: {
402
422
  state: "default"
@@ -847,8 +867,6 @@ var ButtonToolbar = ({ value, children }) => {
847
867
  };
848
868
  var COMMANDS = [
849
869
  "run-operation",
850
- "source freshness",
851
- "docs generate",
852
870
  "run",
853
871
  "test",
854
872
  "seed",
@@ -857,9 +875,13 @@ var COMMANDS = [
857
875
  "ls",
858
876
  "compile",
859
877
  "build",
860
- "deps"
878
+ "deps",
879
+ "docs",
880
+ "source"
861
881
  ];
882
+ var SUBCOMMANDS = { docs: ["generate"], source: ["freshness"] };
862
883
  var ARGUMENTS = ["--select", "--exclude", "--selector", "--defer", "--resource-type", "-s"];
884
+ var SELECTORS = ["--select", "--exclude", "-s"];
863
885
  var METHODS = [
864
886
  "path:",
865
887
  "source:",
@@ -886,13 +908,59 @@ var FLAGS = [
886
908
  "--profile",
887
909
  "--full-refresh"
888
910
  ];
889
- var word = (list) => new RegExp(`^\\s+(?:${list.join("|")})\\b`, "i");
890
- var prefix = (list) => new RegExp(`^\\s+(?:${list.join("|")})`, "i");
911
+ var COMMAND_PHRASES = COMMANDS.flatMap((command) => {
912
+ const subcommands = SUBCOMMANDS[command];
913
+ return subcommands ? subcommands.map((subcommand) => `${command} ${subcommand}`) : [command];
914
+ });
915
+ var alternation = (list) => [...list].sort((a, b) => b.length - a.length).join("|");
916
+ var word = (list) => new RegExp(`^\\s+(?:${alternation(list)})\\b`, "i");
917
+ var prefix = (list) => new RegExp(`^\\s+(?:${alternation(list)})`, "i");
918
+ var WORD = /[\w:-]*$/;
919
+ var asOptions = (list, type) => list.map((label) => ({ label, type }));
920
+ var DBT = [{ label: "dbt", type: "keyword" }];
921
+ var COMMAND_OPTIONS = asOptions(COMMANDS, "keyword");
922
+ var SUBCOMMAND_OPTIONS = Object.fromEntries(
923
+ Object.entries(SUBCOMMANDS).map(([command, subcommands]) => [
924
+ command,
925
+ asOptions(subcommands, "keyword")
926
+ ])
927
+ );
928
+ var ARGUMENT_OPTIONS = asOptions(ARGUMENTS, "variable");
929
+ var FLAG_OPTIONS = asOptions(FLAGS, "constant");
930
+ var METHOD_OPTIONS = asOptions(METHODS, "property");
931
+ var optionsAfter = (words) => {
932
+ const [step, command, ...rest] = words;
933
+ if (step === void 0) return DBT;
934
+ if (step.toLowerCase() !== "dbt") return [];
935
+ if (command === void 0) return COMMAND_OPTIONS;
936
+ const subcommands = SUBCOMMAND_OPTIONS[command.toLowerCase()];
937
+ if (subcommands && rest.length === 0) return subcommands;
938
+ const lastArgument = rest.filter((it) => it.startsWith("-")).at(-1);
939
+ const selecting = lastArgument !== void 0 && SELECTORS.includes(lastArgument.toLowerCase());
940
+ return selecting ? [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS, ...METHOD_OPTIONS] : [...ARGUMENT_OPTIONS, ...FLAG_OPTIONS];
941
+ };
942
+ var complete = (context) => {
943
+ const typed = context.matchBefore(WORD);
944
+ if (!typed?.text && !context.explicit) return null;
945
+ const from = typed?.from ?? context.pos;
946
+ const line = context.state.doc.lineAt(from);
947
+ const options = optionsAfter(
948
+ line.text.slice(0, from - line.from).split(/\s+/).filter(Boolean)
949
+ );
950
+ return options.length > 0 ? { from, options, validFor: WORD } : null;
951
+ };
891
952
  var dbt = simpleMode.simpleMode({
953
+ languageData: {
954
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
955
+ commentTokens: { line: "--", block: { open: "/*", close: "*/" } },
956
+ autocomplete: complete
957
+ },
892
958
  start: [
893
959
  { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
894
- { regex: /dbt/, token: "keyword", sol: true },
895
- { regex: word(COMMANDS), token: "keyword" },
960
+ // Word-bounded, so `dbtX` is not a dbt step whose first word happens to
961
+ // colour — the same thing the completion source asks of it.
962
+ { regex: /dbt\b/, token: "keyword", sol: true },
963
+ { regex: word(COMMAND_PHRASES), token: "keyword" },
896
964
  { regex: word(ARGUMENTS), token: "variableName" },
897
965
  { regex: prefix(METHODS), token: "propertyName" },
898
966
  { regex: prefix(FLAGS), token: "attributeName" },
@@ -940,6 +1008,8 @@ var KEYWORDS = {
940
1008
  };
941
1009
  var jsonnet = {
942
1010
  name: "jsonnet",
1011
+ // `toggleComment` is a no-op without these, so Mod-/ silently does nothing.
1012
+ languageData: { commentTokens: { line: "//", block: { open: "/*", close: "*/" } } },
943
1013
  startState: () => ({
944
1014
  cComment: false,
945
1015
  textBlock: false,
@@ -1072,6 +1142,7 @@ var LANGUAGE_MODES = [
1072
1142
  "text/x-mysql",
1073
1143
  "text/x-pgsql",
1074
1144
  "text/x-mssql",
1145
+ // Snowflake, under a historical name — these strings are a published prop contract.
1075
1146
  "text/x-sfsql",
1076
1147
  "text/x-plsql",
1077
1148
  "text/x-rsrc",
@@ -1133,8 +1204,6 @@ var ALWAYS_OFF = {
1133
1204
  var buildBasicSetup = (readOnly) => ({
1134
1205
  ...ALWAYS_OFF,
1135
1206
  ...readOnly ? {
1136
- history: false,
1137
- historyKeymap: false,
1138
1207
  closeBrackets: false,
1139
1208
  closeBracketsKeymap: false,
1140
1209
  lintKeymap: false,
@@ -1172,7 +1241,7 @@ var getLanguageMode = (mode) => {
1172
1241
  case "text/x-mssql":
1173
1242
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.MSSQL });
1174
1243
  case "text/x-sfsql":
1175
- return language.StreamLanguage.define(codemirrorLangSfsql.sfsql);
1244
+ return langSql.sql({ upperCaseKeywords: true, dialect: codemirrorLangSql.getSqlDialect("snowflake") });
1176
1245
  case "text/x-plsql":
1177
1246
  return langSql.sql({ upperCaseKeywords: true, dialect: langSql.PLSQL });
1178
1247
  case "text/x-rsrc":
@@ -1197,6 +1266,112 @@ var getLanguageMode = (mode) => {
1197
1266
  }
1198
1267
  };
1199
1268
  var shouldLintJson = (mode, readOnly) => mode === "application/json" && !readOnly;
1269
+ var setSearchEffect = state.StateEffect.define();
1270
+ function findMatches(doc, query) {
1271
+ if (!query) return [];
1272
+ const text = doc.toString();
1273
+ try {
1274
+ const regex = new RegExp(escapeStringRegexp(query), "gi");
1275
+ return Array.from(text.matchAll(regex)).map((match2, index) => {
1276
+ const from = match2.index;
1277
+ const to = from + match2[0].length;
1278
+ return { from, to, text: match2[0], localIndex: index };
1279
+ });
1280
+ } catch (e) {
1281
+ console.warn("Invalid search regex:", e);
1282
+ return [];
1283
+ }
1284
+ }
1285
+ function createDecorations(matches, selectedLocalIndex) {
1286
+ const decorations = matches.map((match2) => {
1287
+ const isSelected = match2.localIndex === selectedLocalIndex;
1288
+ const decoration = view.Decoration.mark({
1289
+ class: isSelected ? "cm-searchMatch-selected" : "cm-searchMatch"
1290
+ });
1291
+ return decoration.range(match2.from, match2.to);
1292
+ });
1293
+ return view.Decoration.set(decorations, true);
1294
+ }
1295
+ var searchField = state.StateField.define({
1296
+ create() {
1297
+ return {
1298
+ query: "",
1299
+ selectedMatchIndex: void 0,
1300
+ matches: [],
1301
+ decorations: view.Decoration.none
1302
+ };
1303
+ },
1304
+ update(state, tr) {
1305
+ let newState = state;
1306
+ for (const effect of tr.effects) {
1307
+ if (!effect.is(setSearchEffect)) {
1308
+ continue;
1309
+ }
1310
+ const { query, selectedMatchIndex } = effect.value;
1311
+ const matches = findMatches(tr.state.doc, query);
1312
+ const decorations = createDecorations(matches, selectedMatchIndex);
1313
+ newState = {
1314
+ query,
1315
+ selectedMatchIndex,
1316
+ matches,
1317
+ decorations
1318
+ };
1319
+ }
1320
+ if (tr.docChanged && newState.query) {
1321
+ const matches = findMatches(tr.state.doc, newState.query);
1322
+ const decorations = createDecorations(matches, newState.selectedMatchIndex);
1323
+ newState = {
1324
+ ...newState,
1325
+ matches,
1326
+ decorations
1327
+ };
1328
+ }
1329
+ return newState;
1330
+ },
1331
+ provide: (field) => view.EditorView.decorations.from(field, (state) => state.decorations)
1332
+ });
1333
+ var searchViewPlugin = view.ViewPlugin.fromClass(
1334
+ class {
1335
+ lastSelectedIndex;
1336
+ update(update) {
1337
+ const state = update.state.field(searchField);
1338
+ if (state.selectedMatchIndex !== void 0 && state.selectedMatchIndex !== this.lastSelectedIndex) {
1339
+ const match2 = state.matches[state.selectedMatchIndex];
1340
+ if (match2) {
1341
+ requestAnimationFrame(() => {
1342
+ update.view.dispatch({
1343
+ effects: view.EditorView.scrollIntoView(match2.from, { y: "center" })
1344
+ });
1345
+ });
1346
+ }
1347
+ this.lastSelectedIndex = state.selectedMatchIndex;
1348
+ }
1349
+ if (state.selectedMatchIndex === void 0) {
1350
+ this.lastSelectedIndex = void 0;
1351
+ }
1352
+ }
1353
+ }
1354
+ );
1355
+ var searchTheme = view.EditorView.baseTheme({
1356
+ ".cm-searchMatch": {
1357
+ backgroundColor: colors__default.default.blue[100]
1358
+ },
1359
+ ".cm-searchMatch > span": {
1360
+ color: `${colors__default.default.blue[600]} !important`
1361
+ },
1362
+ ".cm-searchMatch-selected": {
1363
+ backgroundColor: colors__default.default.blue[600]
1364
+ },
1365
+ ".cm-searchMatch-selected > span": {
1366
+ color: `${colors__default.default.white} !important`
1367
+ }
1368
+ });
1369
+ function searchExtension() {
1370
+ return [searchField, searchViewPlugin, searchTheme];
1371
+ }
1372
+ function updateSearch(view, query, selectedMatchIndex) {
1373
+ view.dispatch({ effects: setSearchEffect.of({ query, selectedMatchIndex }) });
1374
+ }
1200
1375
  var KbdSeparator = ({ className, ...props }) => /* @__PURE__ */ jsxRuntime.jsx("span", { ...props, className: cn("tw:text-xs tw:text-muted-foreground", className) });
1201
1376
  var KbdGroup = ({ ref, className, children, separator, ...props }) => {
1202
1377
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -1256,12 +1431,79 @@ Kbd.Group = KbdGroup;
1256
1431
  Kbd.Separator = KbdSeparator;
1257
1432
  var ToggleComponentHelp = () => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1258
1433
  "Use ",
1259
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Ctrl+/" }),
1260
- " or ",
1261
- /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: "Command+/" }),
1434
+ /* @__PURE__ */ jsxRuntime.jsx(Kbd, { children: isMac ? "Command+/" : "Ctrl+/" }),
1262
1435
  " to toggle comments."
1263
1436
  ] });
1264
- var emptyArray = [];
1437
+ 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);
1438
+ var crops = (style) => style.overflowX !== "visible" || style.overflowY !== "visible" || style.maskImage !== "none" || style.contentVisibility !== "visible" || /\b(paint|strict|content)\b/.test(style.contain);
1439
+ var findTooltipHost = (from) => {
1440
+ let outermostCropper = null;
1441
+ for (let node = from.parentElement; node && node !== document.body; node = node.parentElement) {
1442
+ const style = getComputedStyle(node);
1443
+ if (crops(style)) {
1444
+ outermostCropper = node;
1445
+ } else if (establishesFixedContainingBlock(style)) {
1446
+ return outermostCropper?.parentElement ?? node;
1447
+ }
1448
+ }
1449
+ return outermostCropper?.parentElement ?? null;
1450
+ };
1451
+ var tooltipsExtension = () => {
1452
+ const layer = new state.Compartment();
1453
+ return [
1454
+ layer.of(view.tooltips()),
1455
+ view.ViewPlugin.define((view$1) => {
1456
+ const scheduled = setTimeout(() => {
1457
+ if (!view$1.dom.isConnected) return;
1458
+ const parent = findTooltipHost(view$1.dom);
1459
+ if (parent) view$1.dispatch({ effects: layer.reconfigure(view.tooltips({ parent })) });
1460
+ });
1461
+ return { destroy: () => clearTimeout(scheduled) };
1462
+ })
1463
+ ];
1464
+ };
1465
+ var useTooltipsExtension = () => React26.useMemo(tooltipsExtension, []);
1466
+
1467
+ // src/components/CodeEditor/variableCompletion.ts
1468
+ var BOOST = 2e3;
1469
+ var NAME = String.raw`[\w-]*`;
1470
+ var AT_CARET = new RegExp(String.raw`(?:\{{1,2} ?)?${NAME}$`);
1471
+ var NAME_AT_END = new RegExp(`${NAME}$`);
1472
+ var OPENING = /\{{1,2} ?$/;
1473
+ var CLOSING = /^ ?\}{1,2}/;
1474
+ var completeVariables = (variables = []) => {
1475
+ if (variables.length === 0) return () => null;
1476
+ const options = variables.map(({ name, value }) => ({
1477
+ // The bare name: `SqlEditor` filters prefix-only while `CodeEditor` filters
1478
+ // fuzzily, and this is the only spelling both accept whether or not the caret
1479
+ // has typed the braces. The braces are `displayLabel`, which is what renders.
1480
+ label: name,
1481
+ displayLabel: `{{${name}}}`,
1482
+ detail: value,
1483
+ type: "variable",
1484
+ boost: BOOST,
1485
+ apply: (view, completion, from, to) => {
1486
+ const { doc } = view.state;
1487
+ const line = doc.lineAt(from);
1488
+ const insert = `{{${completion.label}}}`;
1489
+ const opening = OPENING.exec(doc.sliceString(line.from, from));
1490
+ const closing = CLOSING.exec(doc.sliceString(to, line.to));
1491
+ const start = opening ? from - opening[0].length : from;
1492
+ const end = closing ? to + closing[0].length : to;
1493
+ view.dispatch({
1494
+ changes: { from: start, to: end, insert },
1495
+ selection: { anchor: start + insert.length }
1496
+ });
1497
+ }
1498
+ }));
1499
+ return (context) => {
1500
+ const match2 = context.matchBefore(AT_CARET);
1501
+ if (!match2?.text) return null;
1502
+ const [name = ""] = NAME_AT_END.exec(match2.text) ?? [];
1503
+ return { from: match2.to - name.length, options };
1504
+ };
1505
+ };
1506
+ var useVariableCompletion = (variables = []) => useStableCallback(completeVariables(variables));
1265
1507
  var emptyObject = {};
1266
1508
  var CodeEditor = ({
1267
1509
  value,
@@ -1270,8 +1512,13 @@ var CodeEditor = ({
1270
1512
  mode = "application/json",
1271
1513
  placeholder = "",
1272
1514
  ariaLabel,
1515
+ searchQuery,
1516
+ searchMatchIndex,
1273
1517
  readOnly = false,
1274
- autocompletions = emptyArray,
1518
+ inputLocked = false,
1519
+ autocompletions,
1520
+ completionSource,
1521
+ variables,
1275
1522
  extraKeys = emptyObject,
1276
1523
  withToggleCommentHint = false,
1277
1524
  withAutofocus = false,
@@ -1286,49 +1533,86 @@ var CodeEditor = ({
1286
1533
  className
1287
1534
  }) => {
1288
1535
  const handleChange = useStableCallback((next) => onChange?.(next));
1536
+ const withSearch = searchQuery !== void 0;
1537
+ const editorRef = React26.useRef(null);
1538
+ React26.useEffect(() => {
1539
+ const view = editorRef.current?.view;
1540
+ if (view && withSearch) updateSearch(view, searchQuery, searchMatchIndex);
1541
+ }, [withSearch, searchQuery, searchMatchIndex]);
1542
+ const handleCreateEditor = useStableCallback((view) => {
1543
+ if (searchQuery) updateSearch(view, searchQuery, searchMatchIndex);
1544
+ });
1545
+ const extraKeysRef = React26.useRef(extraKeys);
1546
+ React26.useLayoutEffect(() => {
1547
+ extraKeysRef.current = extraKeys;
1548
+ });
1549
+ const boundKeysSignature = Object.keys(extraKeys).join("\n");
1550
+ const boundKeys = React26.useMemo(
1551
+ () => boundKeysSignature.split("\n").filter(Boolean),
1552
+ [boundKeysSignature]
1553
+ );
1554
+ const tooltipLayer = useTooltipsExtension();
1555
+ const variableCompletion = useVariableCompletion(variables);
1289
1556
  const customTheme = useThemedExtension("editor:syntax", buildCustomTheme);
1290
1557
  const baseTheme = useThemedExtension(
1291
1558
  "code-editor:chrome",
1292
1559
  () => view.EditorView.theme(buildBaseThemeStyles())
1293
1560
  );
1294
1561
  const commonExtensions = React26.useMemo(() => {
1562
+ const callerCompletions = completionSource ?? (completeAny ? autocomplete.completeAnyWord : autocompletions?.length ? autocompletions : null);
1563
+ const completionEntries = [
1564
+ ...callerCompletions ? [{ autocomplete: callerCompletions }] : [],
1565
+ { autocomplete: variableCompletion }
1566
+ ];
1295
1567
  return [
1296
1568
  getLanguageMode(mode),
1297
1569
  shouldLintJson(mode, readOnly) ? lint.linter(langJson.jsonParseLinter()) : [],
1298
1570
  withLineWrapping ? view.EditorView.lineWrapping : [],
1299
- CodeMirror.EditorState.languageData.of(() => [
1300
- { autocomplete: completeAny ? autocomplete.completeAnyWord : autocompletions }
1301
- ]),
1571
+ CodeMirror.EditorState.languageData.of(() => completionEntries),
1302
1572
  view.keymap.of(
1303
- Object.entries(extraKeys).map(([key, run]) => ({
1573
+ boundKeys.map((key) => ({
1304
1574
  key,
1305
1575
  run: () => {
1306
- if (typeof run === "string") return run;
1307
- run();
1576
+ extraKeysRef.current[key]?.();
1308
1577
  return true;
1309
1578
  }
1310
1579
  }))
1311
1580
  ),
1312
- readOnly ? [] : autocomplete.autocompletion({ activateOnTyping: completeAny }),
1581
+ readOnly ? [] : [
1582
+ // CodeMirror's own default. Gating it on the props hid completions the
1583
+ // *language* supplies — dbt's vocabulary, Snowflake's keywords — behind
1584
+ // a shortcut, and macOS leaves no usable one. With nothing to offer, no
1585
+ // popup opens anyway.
1586
+ autocomplete.autocompletion()
1587
+ ],
1313
1588
  ariaLabel ? view.EditorView.contentAttributes.of({ "aria-label": ariaLabel }) : [],
1589
+ withSearch ? searchExtension() : [],
1590
+ tooltipLayer,
1314
1591
  baseTheme
1315
1592
  ];
1316
1593
  }, [
1317
1594
  autocompletions,
1595
+ completionSource,
1596
+ variableCompletion,
1318
1597
  completeAny,
1319
- extraKeys,
1598
+ boundKeys,
1320
1599
  mode,
1321
1600
  readOnly,
1322
1601
  withLineWrapping,
1323
1602
  baseTheme,
1324
- ariaLabel
1603
+ ariaLabel,
1604
+ withSearch,
1605
+ tooltipLayer
1325
1606
  ]);
1326
1607
  const extensions = React26.useMemo(() => {
1327
1608
  return [
1328
1609
  ...commonExtensions,
1329
1610
  view.placeholder(placeholder),
1330
1611
  view.EditorView.editable.of(!readOnly),
1331
- CodeMirror.EditorState.readOnly.of(readOnly),
1612
+ // The lock is this facet and nothing else. `contenteditable` is what the
1613
+ // read-only wash keys off, and `basicSetup` is what holds the undo history,
1614
+ // so leaving both alone is what makes the lock invisible.
1615
+ CodeMirror.EditorState.readOnly.of(readOnly || inputLocked),
1332
1616
  previousValue ? merge.unifiedMergeView({
1333
1617
  original: previousValue,
1334
1618
  mergeControls: false,
@@ -1336,13 +1620,16 @@ var CodeEditor = ({
1336
1620
  collapseUnchanged: withCollapseUnchanged ? {} : void 0
1337
1621
  }) : []
1338
1622
  ];
1339
- }, [commonExtensions, placeholder, readOnly, previousValue, withCollapseUnchanged]);
1623
+ }, [commonExtensions, placeholder, readOnly, inputLocked, previousValue, withCollapseUnchanged]);
1340
1624
  const config = React26.useMemo(() => {
1341
1625
  return {
1342
1626
  ...buildBasicSetup(readOnly),
1343
- lineNumbers: withLineNumbers
1627
+ lineNumbers: withLineNumbers,
1628
+ // Mod-f opens CodeMirror's own panel, which would run a second query and a
1629
+ // second match count over the same document beside the caller's input.
1630
+ ...withSearch ? { searchKeymap: false } : null
1344
1631
  };
1345
- }, [withLineNumbers, readOnly]);
1632
+ }, [withLineNumbers, readOnly, withSearch]);
1346
1633
  const additionalButtons = renderAdditionalButtons?.(value);
1347
1634
  const hasToolbar = withCopyButton && !!value || !!additionalButtons;
1348
1635
  return (
@@ -1365,6 +1652,8 @@ var CodeEditor = ({
1365
1652
  /* @__PURE__ */ jsxRuntime.jsx(
1366
1653
  CodeMirror__default.default,
1367
1654
  {
1655
+ ref: editorRef,
1656
+ onCreateEditor: handleCreateEditor,
1368
1657
  theme: customTheme,
1369
1658
  value,
1370
1659
  onChange: handleChange,
@@ -8948,9 +9237,9 @@ var ComboboxItemIndicator = ({ ref, className, variant = "check", ...props }) =>
8948
9237
  "tw:absolute",
8949
9238
  "tw:right-2",
8950
9239
  "tw:group-data-[variant=default]/combobox-content:right-4",
8951
- // Lucide SVGs default to 24×24 — constrain the rendered child so the
8952
- // 16px indicator slot owns the actual size.
8953
- 'tw:[&_svg:not([class*="tw:size-"])]:size-4',
9240
+ // `render={<Check />}` makes the icon the indicator element itself (not a descendant), so a
9241
+ // `[&_svg]` rule never matches it — size it directly or Lucide renders at its 24px default.
9242
+ "tw:size-4",
8954
9243
  "tw:group-data-[highlighted]/combobox-item:text-accent-foreground",
8955
9244
  "tw:group-data-[variant=default]/combobox-content:group-data-[highlighted]/combobox-item:text-foreground",
8956
9245
  className
@@ -9075,6 +9364,9 @@ var ComboboxGroupLabel = ({ ref, className, ...props }) => /* @__PURE__ */ jsxRu
9075
9364
  }
9076
9365
  );
9077
9366
  var ComboboxCollection = ({ ...props }) => /* @__PURE__ */ jsxRuntime.jsx(combobox.Combobox.Collection, { "data-slot": "combobox-collection", ...props });
9367
+ var VIRTUALIZE_THRESHOLD = 30;
9368
+ var ESTIMATED_ROW_HEIGHT = 40;
9369
+ var ESTIMATED_LIST_HEIGHT = 320;
9078
9370
  var Combobox = ({
9079
9371
  // root props
9080
9372
  items: _itemsProp,
@@ -9101,6 +9393,7 @@ var Combobox = ({
9101
9393
  empty = "No items",
9102
9394
  indicator,
9103
9395
  prefix: prefix2,
9396
+ virtualized = false,
9104
9397
  // creatable
9105
9398
  allowCreate,
9106
9399
  queryToCreateValue,
@@ -9124,7 +9417,9 @@ var Combobox = ({
9124
9417
  const label = value === null ? "" : itemToStringLabelProp?.(value) ?? toLabel(value);
9125
9418
  setInputValue((prev) => prev === label ? prev : label);
9126
9419
  }, [value]);
9127
- const query = inputValue.trim();
9420
+ const selectedLabel = value == null ? void 0 : itemToStringLabelProp?.(value) ?? toLabel(value);
9421
+ const isShowingSelection = selectedLabel !== void 0 && inputValue === selectedLabel;
9422
+ const query = (isShowingSelection ? "" : inputValue).trim();
9128
9423
  const queryLower = query.toLowerCase();
9129
9424
  const itemsProp = _itemsProp ?? [];
9130
9425
  const sampleItem = itemsProp.flatMap((item) => isGroupItem(item) ? item.items : [item])[0];
@@ -9149,7 +9444,7 @@ var Combobox = ({
9149
9444
  if (isCreatableItem(value2)) return value2.value;
9150
9445
  return itemToStringLabelProp?.(value2) ?? toLabel(value2);
9151
9446
  };
9152
- const wrappedFilter = !allowCreate || !filter ? filter : (item, ...args) => isCreatableItem(item) || filter(item, ...args);
9447
+ const wrappedFilter = isShowingSelection ? () => true : !allowCreate || !filter ? filter : (item, ...args) => isCreatableItem(item) || filter(item, ...args);
9153
9448
  const wrappedItemToStringValue = !allowCreate || !itemToStringValue ? itemToStringValue : (item) => isCreatableItem(item) ? item.value : itemToStringValue(item);
9154
9449
  const isItemEqualToValue = (a, b) => {
9155
9450
  if (isCreatableItem(a) || isCreatableItem(b)) return toValueKey(a) === toValueKey(b);
@@ -9184,10 +9479,34 @@ var Combobox = ({
9184
9479
  closeReasonRef.current = "item-press";
9185
9480
  onValueChange?.(next, event);
9186
9481
  };
9187
- const renderItem = (item) => {
9482
+ const [listElement, setListElement] = React26.useState(null);
9483
+ const isGrouped = isGroupItems(allItems);
9484
+ const shouldVirtualize = !isGrouped && (virtualized === true || virtualized === "auto" && allItems.length > VIRTUALIZE_THRESHOLD);
9485
+ const matchesQuery = (item) => {
9486
+ if (isCreatableItem(item)) return true;
9487
+ if (!query) return true;
9488
+ if (filter) return !!filter(item, query, (value2) => itemToStringLabel(value2) ?? toLabel(value2));
9489
+ return (itemToStringLabel(item) ?? toLabel(item)).toLowerCase().includes(queryLower);
9490
+ };
9491
+ const virtualItems = shouldVirtualize ? items.filter(matchesQuery) : [];
9492
+ const virtualizer = reactVirtual.useVirtualizer({
9493
+ count: virtualItems.length,
9494
+ getScrollElement: () => listElement,
9495
+ estimateSize: () => ESTIMATED_ROW_HEIGHT,
9496
+ overscan: 12,
9497
+ enabled: shouldVirtualize,
9498
+ // Seed a viewport height so rows paint on the first frame. Without it the list measures 0 until
9499
+ // the ResizeObserver fires (a frame later), which renders an empty popup on open.
9500
+ initialRect: { width: 0, height: ESTIMATED_LIST_HEIGHT }
9501
+ });
9502
+ const renderItem = (item, index) => {
9503
+ if (isCreatableItem(item)) {
9504
+ return /* @__PURE__ */ jsxRuntime.jsx(Combobox.Item, { index, value: item, icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, {}), children: queryToCreateLabel(item.value) });
9505
+ }
9188
9506
  return /* @__PURE__ */ jsxRuntime.jsx(
9189
9507
  Combobox.Item,
9190
9508
  {
9509
+ index,
9191
9510
  value: item,
9192
9511
  indicator,
9193
9512
  icon: itemToIcon(item),
@@ -9196,13 +9515,20 @@ var Combobox = ({
9196
9515
  }
9197
9516
  );
9198
9517
  };
9518
+ const rowStyle = (offset) => ({
9519
+ position: "absolute",
9520
+ top: 0,
9521
+ left: 0,
9522
+ width: "100%",
9523
+ transform: `translateY(${offset}px)`
9524
+ });
9199
9525
  return /* @__PURE__ */ jsxRuntime.jsxs(
9200
9526
  combobox.Combobox.Root,
9201
9527
  {
9202
- items,
9528
+ items: shouldVirtualize ? virtualItems : items,
9203
9529
  value,
9204
9530
  defaultValue,
9205
- filter: wrappedFilter,
9531
+ filter: shouldVirtualize ? null : wrappedFilter,
9206
9532
  onValueChange: handleOnValueChange,
9207
9533
  itemToStringLabel,
9208
9534
  isItemEqualToValue,
@@ -9212,6 +9538,12 @@ var Combobox = ({
9212
9538
  inputRef,
9213
9539
  inputValue,
9214
9540
  onInputValueChange: handleInputValueChange,
9541
+ virtualized: shouldVirtualize,
9542
+ onItemHighlighted: (_, details) => {
9543
+ if (shouldVirtualize && details.reason === "keyboard") {
9544
+ virtualizer.scrollToIndex(details.index);
9545
+ }
9546
+ },
9215
9547
  children: [
9216
9548
  /* @__PURE__ */ jsxRuntime.jsxs(
9217
9549
  ComboboxInputGroup,
@@ -9233,7 +9565,30 @@ var Combobox = ({
9233
9565
  ),
9234
9566
  /* @__PURE__ */ jsxRuntime.jsxs(ComboboxContent, { children: [
9235
9567
  /* @__PURE__ */ jsxRuntime.jsx(ComboboxEmpty, { children: empty }),
9236
- /* @__PURE__ */ jsxRuntime.jsx(ComboboxList, { children: (itemOrGroup) => {
9568
+ shouldVirtualize ? /* @__PURE__ */ jsxRuntime.jsx(ComboboxList, { ref: setListElement, children: /* @__PURE__ */ jsxRuntime.jsx(
9569
+ "div",
9570
+ {
9571
+ style: {
9572
+ position: "relative",
9573
+ width: "100%",
9574
+ height: `${virtualizer.getTotalSize()}px`
9575
+ },
9576
+ children: virtualizer.getVirtualItems().map((row) => {
9577
+ const item = virtualItems[row.index];
9578
+ if (item === void 0) return null;
9579
+ return /* @__PURE__ */ jsxRuntime.jsx(
9580
+ "div",
9581
+ {
9582
+ "data-index": row.index,
9583
+ ref: virtualizer.measureElement,
9584
+ style: rowStyle(row.start),
9585
+ children: renderItem(item, row.index)
9586
+ },
9587
+ row.key
9588
+ );
9589
+ })
9590
+ }
9591
+ ) }) : /* @__PURE__ */ jsxRuntime.jsx(ComboboxList, { children: (itemOrGroup) => {
9237
9592
  if (isCreatableItem(itemOrGroup))
9238
9593
  return /* @__PURE__ */ jsxRuntime.jsx(Combobox.Item, { value: itemOrGroup, icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, {}), children: queryToCreateLabel(itemOrGroup.value) });
9239
9594
  if (isGroupItem(itemOrGroup)) {
@@ -9244,7 +9599,7 @@ var Combobox = ({
9244
9599
  const groupLabel = groupToLabel(itemOrGroup);
9245
9600
  return /* @__PURE__ */ jsxRuntime.jsxs(Combobox.Group, { items: itemOrGroup.items, children: [
9246
9601
  groupLabel && /* @__PURE__ */ jsxRuntime.jsx(Combobox.GroupLabel, { children: groupLabel }),
9247
- /* @__PURE__ */ jsxRuntime.jsx(Combobox.GroupList, { children: renderItem })
9602
+ /* @__PURE__ */ jsxRuntime.jsx(Combobox.GroupList, { children: (item) => renderItem(item) })
9248
9603
  ] });
9249
9604
  }
9250
9605
  return renderItem(itemOrGroup);