@storybook/react-native-ui 10.2.1 → 10.2.2-alpha.1

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/index.js CHANGED
@@ -726,14 +726,14 @@ var Explorer = import_react7.default.memo(function Explorer2({
726
726
  });
727
727
 
728
728
  // src/Sidebar.tsx
729
- var import_react12 = __toESM(require("react"));
729
+ var import_react13 = __toESM(require("react"));
730
730
  var import_react_native_theming10 = require("@storybook/react-native-theming");
731
731
 
732
732
  // src/Search.tsx
733
733
  var import_bottom_sheet = require("@gorhom/bottom-sheet");
734
734
  var import_react_native_theming8 = require("@storybook/react-native-theming");
735
- var import_fuse = __toESM(require("fuse.js"));
736
- var import_react9 = __toESM(require("react"));
735
+ var import_react9 = require("@nozbe/microfuzz/react");
736
+ var import_react10 = __toESM(require("react"));
737
737
  var import_react_native2 = require("react-native");
738
738
 
739
739
  // src/icon/CloseIcon.tsx
@@ -785,22 +785,6 @@ var SearchIcon = ({ width = 14, height = 14, ...props }) => {
785
785
  var import_react_native_ui_common3 = require("@storybook/react-native-ui-common");
786
786
  var import_jsx_runtime14 = require("react/jsx-runtime");
787
787
  var DEFAULT_MAX_SEARCH_RESULTS = 50;
788
- var options = {
789
- shouldSort: true,
790
- tokenize: true,
791
- findAllMatches: true,
792
- includeScore: true,
793
- includeMatches: true,
794
- threshold: 0.2,
795
- location: 0,
796
- distance: 100,
797
- maxPatternLength: 32,
798
- minMatchCharLength: 1,
799
- keys: [
800
- { name: "name", weight: 0.7 },
801
- { name: "path", weight: 0.3 }
802
- ]
803
- };
804
788
  var SearchIconWrapper = import_react_native_theming8.styled.View({
805
789
  position: "absolute",
806
790
  top: 0,
@@ -856,15 +840,15 @@ var ClearIcon = import_react_native_theming8.styled.TouchableOpacity(({ theme })
856
840
  justifyContent: "center",
857
841
  height: "100%"
858
842
  }));
859
- var Search = import_react9.default.memo(function Search2({ children, dataset, setSelection, getLastViewed, initialQuery = "" }) {
843
+ var Search = import_react10.default.memo(function Search2({ children, dataset, setSelection, getLastViewed, initialQuery = "" }) {
860
844
  const context = (0, import_bottom_sheet.useBottomSheetInternal)(true);
861
845
  const isBottomSheet = context !== null;
862
- const inputRef = (0, import_react9.useRef)(null);
863
- const [inputValue, setInputValue] = (0, import_react9.useState)(initialQuery);
864
- const [isOpen, setIsOpen] = (0, import_react9.useState)(false);
865
- const [allComponents, showAllComponents] = (0, import_react9.useState)(false);
846
+ const inputRef = (0, import_react10.useRef)(null);
847
+ const [inputValue, setInputValue] = (0, import_react10.useState)(initialQuery);
848
+ const [isOpen, setIsOpen] = (0, import_react10.useState)(false);
849
+ const [allComponents, showAllComponents] = (0, import_react10.useState)(false);
866
850
  const { scrollToSelectedNode } = useSelectedNode();
867
- const selectStory = (0, import_react9.useCallback)(
851
+ const selectStory = (0, import_react10.useCallback)(
868
852
  (id, refId) => {
869
853
  setSelection({ storyId: id, refId });
870
854
  inputRef.current?.blur();
@@ -874,11 +858,10 @@ var Search = import_react9.default.memo(function Search2({ children, dataset, se
874
858
  },
875
859
  [scrollToSelectedNode, setSelection]
876
860
  );
877
- const getItemProps = (0, import_react9.useCallback)(
861
+ const getItemProps = (0, import_react10.useCallback)(
878
862
  ({ item: result }) => {
879
863
  return {
880
864
  icon: result?.item?.type === "component" ? "component" : "story",
881
- result,
882
865
  onPress: () => {
883
866
  if (result?.item?.type === "story") {
884
867
  selectStory(result.item.id, result.item.refId);
@@ -889,7 +872,6 @@ var Search = import_react9.default.memo(function Search2({ children, dataset, se
889
872
  }
890
873
  },
891
874
  score: result.score,
892
- refIndex: result.refIndex,
893
875
  item: result.item,
894
876
  matches: result.matches,
895
877
  isHighlighted: false
@@ -897,63 +879,69 @@ var Search = import_react9.default.memo(function Search2({ children, dataset, se
897
879
  },
898
880
  [selectStory]
899
881
  );
900
- const makeFuse = (0, import_react9.useCallback)(() => {
901
- const list = dataset.entries.reduce((acc, [refId, { index }]) => {
882
+ const deferredDataset = (0, import_react10.useDeferredValue)(dataset);
883
+ const searchList = (0, import_react10.useMemo)(() => {
884
+ return deferredDataset.entries.reduce((acc, [refId, { index }]) => {
902
885
  if (index) {
903
886
  acc.push(
904
887
  ...Object.values(index).map((item) => {
905
- return (0, import_react_native_ui_common3.searchItem)(item, dataset.hash[refId]);
888
+ return (0, import_react_native_ui_common3.searchItem)(item, deferredDataset.hash[refId]);
906
889
  })
907
890
  );
908
891
  }
909
892
  return acc;
910
893
  }, []);
911
- return new import_fuse.default(list, options);
912
- }, [dataset]);
913
- const getResults = (0, import_react9.useCallback)(
914
- (input2) => {
915
- const fuse = makeFuse();
916
- if (!input2) return [];
917
- let results2 = [];
918
- const resultIds = /* @__PURE__ */ new Set();
919
- const distinctResults = fuse.search(input2).filter(({ item }) => {
920
- if (!(item.type === "component" || item.type === "docs" || item.type === "story") || resultIds.has(item.parent)) {
921
- return false;
922
- }
923
- resultIds.add(item.id);
924
- return true;
925
- });
926
- if (distinctResults.length) {
927
- results2 = distinctResults.slice(0, allComponents ? 1e3 : DEFAULT_MAX_SEARCH_RESULTS);
928
- if (distinctResults.length > DEFAULT_MAX_SEARCH_RESULTS && !allComponents) {
929
- results2.push({
930
- showAll: () => showAllComponents(true),
931
- totalCount: distinctResults.length,
932
- moreCount: distinctResults.length - DEFAULT_MAX_SEARCH_RESULTS
933
- });
934
- }
894
+ }, [deferredDataset]);
895
+ const deferredQuery = (0, import_react10.useDeferredValue)(inputValue);
896
+ const queryText = (0, import_react10.useMemo)(() => deferredQuery ? deferredQuery.trim() : "", [deferredQuery]);
897
+ const getText = (0, import_react10.useCallback)((item) => [item.name, item.path?.join(" ") ?? ""], []);
898
+ const mapResultItem = (0, import_react10.useCallback)(
899
+ ({
900
+ item,
901
+ score,
902
+ matches
903
+ }) => ({
904
+ item,
905
+ score,
906
+ matches: matches ?? []
907
+ }),
908
+ []
909
+ );
910
+ const fuzzyResults = (0, import_react9.useFuzzySearchList)({
911
+ list: searchList,
912
+ queryText,
913
+ getText,
914
+ mapResultItem
915
+ });
916
+ const results = (0, import_react10.useMemo)(() => {
917
+ if (!queryText) return [];
918
+ const maxResults = allComponents ? 1e3 : DEFAULT_MAX_SEARCH_RESULTS;
919
+ const processedResults = [];
920
+ const resultIds = /* @__PURE__ */ new Set();
921
+ let totalDistinctCount = 0;
922
+ for (const result of fuzzyResults) {
923
+ const { item } = result;
924
+ if (!(item.type === "component" || item.type === "docs" || item.type === "story") || resultIds.has(item.parent)) {
925
+ continue;
935
926
  }
936
- const lastViewed = !input2 && getLastViewed();
937
- if (lastViewed && lastViewed.length) {
938
- results2 = lastViewed.reduce((acc, { storyId, refId }) => {
939
- const data = dataset.hash[refId];
940
- if (data && data.index && data.index[storyId]) {
941
- const story = data.index[storyId];
942
- const item = story.type === "story" ? data.index[story.parent] : story;
943
- if (!acc.some((res) => res.item.refId === refId && res.item.id === item.id)) {
944
- acc.push({ item: (0, import_react_native_ui_common3.searchItem)(item, dataset.hash[refId]), matches: [], score: 0 });
945
- }
946
- }
947
- return acc;
948
- }, []);
927
+ resultIds.add(item.id);
928
+ totalDistinctCount++;
929
+ if (processedResults.length < maxResults) {
930
+ processedResults.push(result);
949
931
  }
950
- return results2;
951
- },
952
- [allComponents, dataset.hash, getLastViewed, makeFuse]
953
- );
954
- const deferredQuery = (0, import_react9.useDeferredValue)(inputValue);
955
- const input = deferredQuery ? deferredQuery.trim() : "";
956
- const results = input ? getResults(input) : [];
932
+ if (allComponents && processedResults.length >= maxResults) {
933
+ break;
934
+ }
935
+ }
936
+ if (!allComponents && totalDistinctCount > DEFAULT_MAX_SEARCH_RESULTS) {
937
+ processedResults.push({
938
+ showAll: () => showAllComponents(true),
939
+ totalCount: totalDistinctCount,
940
+ moreCount: totalDistinctCount - DEFAULT_MAX_SEARCH_RESULTS
941
+ });
942
+ }
943
+ return processedResults;
944
+ }, [queryText, fuzzyResults, allComponents]);
957
945
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_react_native2.View, { style: { flex: 1 }, children: [
958
946
  /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(SearchField, { children: [
959
947
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchIconWrapper, { children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SearchIcon, {}) }),
@@ -978,7 +966,7 @@ var Search = import_react9.default.memo(function Search2({ children, dataset, se
978
966
  )
979
967
  ] }),
980
968
  children({
981
- query: input,
969
+ query: queryText,
982
970
  results,
983
971
  isBrowsing: !isOpen || !inputValue.length,
984
972
  closeMenu: () => {
@@ -991,12 +979,12 @@ var Search = import_react9.default.memo(function Search2({ children, dataset, se
991
979
 
992
980
  // src/SearchResults.tsx
993
981
  var import_react_native_theming9 = require("@storybook/react-native-theming");
994
- var import_react10 = __toESM(require("react"));
982
+ var import_react11 = __toESM(require("react"));
995
983
  var import_polished2 = require("polished");
996
984
  var import_react_native_ui_common4 = require("@storybook/react-native-ui-common");
997
985
  var import_react_native3 = require("react-native");
998
986
  var import_jsx_runtime15 = require("react/jsx-runtime");
999
- var import_react11 = require("react");
987
+ var import_react12 = require("react");
1000
988
  var ResultsList = import_react_native_theming9.styled.View({
1001
989
  margin: 0,
1002
990
  padding: 0,
@@ -1062,22 +1050,23 @@ var RecentlyOpenedTitle = import_react_native_theming9.styled.View(({ theme }) =
1062
1050
  marginBottom: 4,
1063
1051
  alignItems: "center"
1064
1052
  }));
1065
- var Highlight = import_react10.default.memo(
1066
- function Highlight2({ children, match }) {
1067
- if (!match) return children;
1068
- const { value, indices } = match;
1069
- const { nodes: result } = indices.reduce(
1053
+ var Highlight = import_react11.default.memo(
1054
+ function Highlight2({ children, text, ranges }) {
1055
+ if (!ranges || ranges.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: children ?? text });
1056
+ const { nodes: result } = ranges.reduce(
1070
1057
  ({ cursor, nodes }, [start, end], index, { length }) => {
1071
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: value.slice(cursor, start) }, `text-${index}`));
1072
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Mark, { children: value.slice(start, end + 1) }, `mark-${index}`));
1073
- if (index === length - 1) {
1074
- nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: value.slice(end + 1) }, `last-${index}`));
1058
+ if (cursor < start) {
1059
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: text.slice(cursor, start) }, `text-${index}`));
1060
+ }
1061
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Mark, { children: text.slice(start, end + 1) }, `mark-${index}`));
1062
+ if (index === length - 1 && end + 1 < text.length) {
1063
+ nodes.push(/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: text.slice(end + 1) }, `last-${index}`));
1075
1064
  }
1076
1065
  return { cursor: end + 1, nodes };
1077
1066
  },
1078
1067
  { cursor: 0, nodes: [] }
1079
1068
  );
1080
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: result }, `end-${match.key}`);
1069
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Text, { children: result });
1081
1070
  }
1082
1071
  );
1083
1072
  var Title = import_react_native_theming9.styled.Text(({ theme }) => ({
@@ -1096,46 +1085,37 @@ var PathText = import_react_native_theming9.styled.Text(({ theme }) => ({
1096
1085
  fontSize: theme.typography.size.s1 - 1,
1097
1086
  color: theme.textMutedColor
1098
1087
  }));
1099
- var Result = import_react10.default.memo(function Result2({
1088
+ var Result = import_react11.default.memo(function Result2({
1100
1089
  item,
1101
1090
  matches,
1102
1091
  icon: _icon,
1103
1092
  onPress,
1104
1093
  ...props
1105
1094
  }) {
1106
- const press = (0, import_react10.useCallback)(
1095
+ const press = (0, import_react11.useCallback)(
1107
1096
  (event) => {
1108
1097
  event.preventDefault();
1109
1098
  onPress?.(event);
1110
1099
  },
1111
1100
  [onPress]
1112
1101
  );
1113
- const nameMatch = matches.find((match) => match.key === "name");
1114
- const pathMatches = matches.filter((match) => match.key === "path");
1102
+ const nameHighlights = matches?.[0];
1103
+ const pathString = item.path?.join(" ") ?? "";
1115
1104
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ResultRow, { ...props, onPress: press, children: [
1116
1105
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(IconWrapper, { children: [
1117
1106
  item.type === "component" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ComponentIcon, { width: "14", height: "14" }),
1118
1107
  item.type === "story" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(StoryIcon, { width: "14", height: "14" })
1119
1108
  ] }),
1120
1109
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(ResultRowContent, { testID: "search-result-item--label", children: [
1121
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Title, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Highlight, { match: nameMatch, children: item.name }, "search-result-item--label-highlight") }),
1122
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Path9, { children: item.path.map((group, index) => {
1123
- const pathSeparator = index === item.path.length - 1 ? "" : "/";
1124
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_react_native3.View, { style: { flexShrink: 1 }, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PathText, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
1125
- Highlight,
1126
- {
1127
- match: pathMatches.find((match) => match.refIndex === index),
1128
- children: `${group}${pathSeparator}`
1129
- }
1130
- ) }) }, index);
1131
- }) })
1110
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Title, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Highlight, { text: item.name, ranges: nameHighlights, children: item.name }) }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Path9, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PathText, { children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Highlight, { text: pathString, ranges: matches?.[1], children: item.path?.join(" / ") }) }) })
1132
1112
  ] })
1133
1113
  ] });
1134
1114
  });
1135
1115
  var Text = import_react_native_theming9.styled.Text(({ theme }) => ({
1136
1116
  color: theme.color.defaultText
1137
1117
  }));
1138
- var SearchResults = import_react10.default.memo(function SearchResults2({
1118
+ var SearchResults = import_react11.default.memo(function SearchResults2({
1139
1119
  query,
1140
1120
  results,
1141
1121
  closeMenu,
@@ -1170,7 +1150,7 @@ var SearchResults = import_react10.default.memo(function SearchResults2({
1170
1150
  }
1171
1151
  const { item } = result;
1172
1152
  const key = `${item.refId}::${item.id}`;
1173
- return /* @__PURE__ */ (0, import_react11.createElement)(
1153
+ return /* @__PURE__ */ (0, import_react12.createElement)(
1174
1154
  Result,
1175
1155
  {
1176
1156
  ...result,
@@ -1208,18 +1188,18 @@ var Top = import_react_native_theming10.styled.View({
1208
1188
  flex: 1,
1209
1189
  flexDirection: "row"
1210
1190
  });
1211
- var Swap = import_react12.default.memo(function Swap2({
1191
+ var Swap = import_react13.default.memo(function Swap2({
1212
1192
  children,
1213
1193
  condition
1214
1194
  }) {
1215
- const [a, b] = import_react12.default.Children.toArray(children);
1195
+ const [a, b] = import_react13.default.Children.toArray(children);
1216
1196
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
1217
1197
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native4.View, { style: { display: condition ? "flex" : "none" }, children: a }),
1218
1198
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_react_native4.View, { style: { display: condition ? "none" : "flex" }, children: b })
1219
1199
  ] });
1220
1200
  });
1221
1201
  var useCombination = (index, indexError, previewInitialized, status, refs) => {
1222
- const hash = (0, import_react12.useMemo)(
1202
+ const hash = (0, import_react13.useMemo)(
1223
1203
  () => ({
1224
1204
  [DEFAULT_REF_ID]: {
1225
1205
  index,
@@ -1234,9 +1214,9 @@ var useCombination = (index, indexError, previewInitialized, status, refs) => {
1234
1214
  }),
1235
1215
  [refs, index, indexError, previewInitialized, status]
1236
1216
  );
1237
- return (0, import_react12.useMemo)(() => ({ hash, entries: Object.entries(hash) }), [hash]);
1217
+ return (0, import_react13.useMemo)(() => ({ hash, entries: Object.entries(hash) }), [hash]);
1238
1218
  };
1239
- var Sidebar = import_react12.default.memo(function Sidebar2({
1219
+ var Sidebar = import_react13.default.memo(function Sidebar2({
1240
1220
  storyId = null,
1241
1221
  refId = DEFAULT_REF_ID,
1242
1222
  index,
@@ -1246,7 +1226,7 @@ var Sidebar = import_react12.default.memo(function Sidebar2({
1246
1226
  refs = {},
1247
1227
  setSelection
1248
1228
  }) {
1249
- const selected = (0, import_react12.useMemo)(() => storyId && { storyId, refId }, [storyId, refId]);
1229
+ const selected = (0, import_react13.useMemo)(() => storyId && { storyId, refId }, [storyId, refId]);
1250
1230
  const dataset = useCombination(index, indexError, previewInitialized, status, refs);
1251
1231
  const lastViewedProps = (0, import_react_native_ui_common5.useLastViewed)(selected);
1252
1232
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Container2, { style: { paddingHorizontal: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Top, { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Search, { dataset, setSelection, ...lastViewedProps, children: ({ query, results, isBrowsing, closeMenu, getItemProps, highlightedIndex }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Swap, { condition: isBrowsing, children: [
@@ -1280,7 +1260,7 @@ var import_bottom_sheet4 = require("@gorhom/bottom-sheet");
1280
1260
  var import_portal = require("@gorhom/portal");
1281
1261
  var import_react_native_theming15 = require("@storybook/react-native-theming");
1282
1262
  var import_react_native_ui_common7 = require("@storybook/react-native-ui-common");
1283
- var import_react18 = require("react");
1263
+ var import_react19 = require("react");
1284
1264
  var import_react_native8 = require("react-native");
1285
1265
  var import_react_native_gesture_handler2 = require("react-native-gesture-handler");
1286
1266
  var import_react_native_safe_area_context3 = require("react-native-safe-area-context");
@@ -1317,13 +1297,13 @@ var BottomBarToggleIcon = ({
1317
1297
  };
1318
1298
 
1319
1299
  // src/icon/CloseFullscreenIcon.tsx
1320
- var import_react13 = require("react");
1300
+ var import_react14 = require("react");
1321
1301
  var import_react_native_svg10 = __toESM(require("react-native-svg"));
1322
1302
  var import_react_native_theming11 = require("@storybook/react-native-theming");
1323
1303
  var import_jsx_runtime18 = require("react/jsx-runtime");
1324
1304
  function CloseFullscreenIcon({ color, width = 14, height = 14, ...props }) {
1325
1305
  const theme = (0, import_react_native_theming11.useTheme)();
1326
- const fillColor = (0, import_react13.useMemo)(() => {
1306
+ const fillColor = (0, import_react14.useMemo)(() => {
1327
1307
  return color ?? theme.color.defaultText;
1328
1308
  }, [color, theme.color.defaultText]);
1329
1309
  return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_svg10.default, { fill: fillColor, height, viewBox: "0 0 16 16", width, ...props, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(import_react_native_svg10.Path, { d: "M5.5 0a.5.5 0 01.5.5v4A1.5 1.5 0 014.5 6h-4a.5.5 0 010-1h4a.5.5 0 00.5-.5v-4a.5.5 0 01.5-.5zm5 0a.5.5 0 01.5.5v4a.5.5 0 00.5.5h4a.5.5 0 010 1h-4A1.5 1.5 0 0110 4.5v-4a.5.5 0 01.5-.5zM0 10.5a.5.5 0 01.5-.5h4A1.5 1.5 0 016 11.5v4a.5.5 0 01-1 0v-4a.5.5 0 00-.5-.5h-4a.5.5 0 01-.5-.5zm10 1a1.5 1.5 0 011.5-1.5h4a.5.5 0 010 1h-4a.5.5 0 00-.5.5v4a.5.5 0 01-1 0v-4z" }) });
@@ -1332,11 +1312,11 @@ function CloseFullscreenIcon({ color, width = 14, height = 14, ...props }) {
1332
1312
  // src/icon/FullscreenIcon.tsx
1333
1313
  var import_react_native_svg11 = __toESM(require("react-native-svg"));
1334
1314
  var import_react_native_theming12 = require("@storybook/react-native-theming");
1335
- var import_react14 = require("react");
1315
+ var import_react15 = require("react");
1336
1316
  var import_jsx_runtime19 = require("react/jsx-runtime");
1337
1317
  function FullscreenIcon({ color, width = 14, height = 14, ...props }) {
1338
1318
  const theme = (0, import_react_native_theming12.useTheme)();
1339
- const fillColor = (0, import_react14.useMemo)(() => {
1319
+ const fillColor = (0, import_react15.useMemo)(() => {
1340
1320
  return color ?? theme.color.defaultText;
1341
1321
  }, [color, theme.color.defaultText]);
1342
1322
  return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(import_react_native_svg11.default, { width, height, viewBox: "0 0 14 14", fill: "none", ...props, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
@@ -1371,7 +1351,7 @@ var import_bottom_sheet2 = require("@gorhom/bottom-sheet");
1371
1351
  var import_manager_api = require("storybook/manager-api");
1372
1352
  var import_react_native_theming13 = require("@storybook/react-native-theming");
1373
1353
  var import_types = require("storybook/internal/types");
1374
- var import_react15 = require("react");
1354
+ var import_react16 = require("react");
1375
1355
  var import_react_native5 = require("react-native");
1376
1356
  var import_react_native_gesture_handler = require("react-native-gesture-handler");
1377
1357
  var import_react_native_reanimated = __toESM(require("react-native-reanimated"));
@@ -1384,15 +1364,15 @@ var bottomSheetStyle = {
1384
1364
  var contentStyle = {
1385
1365
  flex: 1
1386
1366
  };
1387
- var MobileAddonsPanel = (0, import_react15.forwardRef)(
1367
+ var MobileAddonsPanel = (0, import_react16.forwardRef)(
1388
1368
  ({ storyId }, ref) => {
1389
1369
  const theme = (0, import_react_native_theming13.useTheme)();
1390
1370
  const reducedMotion = (0, import_react_native_reanimated.useReducedMotion)();
1391
- const addonsPanelBottomSheetRef = (0, import_react15.useRef)(null);
1371
+ const addonsPanelBottomSheetRef = (0, import_react16.useRef)(null);
1392
1372
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
1393
1373
  const animatedPosition = (0, import_react_native_reanimated.useSharedValue)(0);
1394
1374
  (0, import_react_native_reanimated.useAnimatedKeyboard)();
1395
- (0, import_react15.useImperativeHandle)(ref, () => ({
1375
+ (0, import_react16.useImperativeHandle)(ref, () => ({
1396
1376
  setAddonsPanelOpen: (open) => {
1397
1377
  if (open) {
1398
1378
  addonsPanelBottomSheetRef.current?.present();
@@ -1478,14 +1458,14 @@ var centeredStyle = {
1478
1458
  var hitSlop = { top: 10, right: 10, bottom: 10, left: 10 };
1479
1459
  var AddonsTabs = ({ onClose, storyId }) => {
1480
1460
  const panels = import_manager_api.addons.getElements(import_types.Addon_TypesEnum.PANEL);
1481
- const [addonSelected, setAddonSelected] = (0, import_react15.useState)(Object.keys(panels)[0]);
1461
+ const [addonSelected, setAddonSelected] = (0, import_react16.useState)(Object.keys(panels)[0]);
1482
1462
  const insets = (0, import_react_native_safe_area_context.useSafeAreaInsets)();
1483
1463
  const scrollContentContainerStyle = (0, import_react_native_ui_common6.useStyle)(() => {
1484
1464
  return {
1485
1465
  paddingBottom: insets.bottom + 16
1486
1466
  };
1487
1467
  });
1488
- const panel = (0, import_react15.useMemo)(() => {
1468
+ const panel = (0, import_react16.useMemo)(() => {
1489
1469
  if (!storyId) {
1490
1470
  return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native5.View, { style: centeredStyle, children: /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(import_react_native5.Text, { children: "No Story Selected" }) });
1491
1471
  }
@@ -1558,7 +1538,7 @@ var TabText = import_react_native_theming13.styled.Text(({ theme, active }) => (
1558
1538
  // src/MobileMenuDrawer.tsx
1559
1539
  var import_bottom_sheet3 = __toESM(require("@gorhom/bottom-sheet"));
1560
1540
  var import_react_native_theming14 = require("@storybook/react-native-theming");
1561
- var import_react16 = require("react");
1541
+ var import_react17 = require("react");
1562
1542
  var import_react_native6 = require("react-native");
1563
1543
  var import_react_native_reanimated2 = require("react-native-reanimated");
1564
1544
  var import_react_native_safe_area_context2 = require("react-native-safe-area-context");
@@ -1591,15 +1571,15 @@ var BottomSheetBackdropComponent = (backdropComponentProps) => {
1591
1571
  );
1592
1572
  };
1593
1573
  var snapPoints = ["50%", "75%"];
1594
- var MobileMenuDrawer = (0, import_react16.memo)(
1595
- (0, import_react16.forwardRef)(({ children }, ref) => {
1574
+ var MobileMenuDrawer = (0, import_react17.memo)(
1575
+ (0, import_react17.forwardRef)(({ children }, ref) => {
1596
1576
  const reducedMotion = (0, import_react_native_reanimated2.useReducedMotion)();
1597
1577
  const insets = (0, import_react_native_safe_area_context2.useSafeAreaInsets)();
1598
1578
  const theme = (0, import_react_native_theming14.useTheme)();
1599
- const menuBottomSheetRef = (0, import_react16.useRef)(null);
1579
+ const menuBottomSheetRef = (0, import_react17.useRef)(null);
1600
1580
  const { scrollToSelectedNode, scrollRef } = useSelectedNode();
1601
- const shouldScrollOnOpen = (0, import_react16.useRef)(false);
1602
- const handleSheetChange = (0, import_react16.useCallback)(
1581
+ const shouldScrollOnOpen = (0, import_react17.useRef)(false);
1582
+ const handleSheetChange = (0, import_react17.useCallback)(
1603
1583
  (index) => {
1604
1584
  if (index >= 0 && shouldScrollOnOpen.current) {
1605
1585
  shouldScrollOnOpen.current = false;
@@ -1608,7 +1588,7 @@ var MobileMenuDrawer = (0, import_react16.memo)(
1608
1588
  },
1609
1589
  [scrollToSelectedNode]
1610
1590
  );
1611
- (0, import_react16.useImperativeHandle)(ref, () => ({
1591
+ (0, import_react17.useImperativeHandle)(ref, () => ({
1612
1592
  setMobileMenuOpen: (open) => {
1613
1593
  if (open) {
1614
1594
  shouldScrollOnOpen.current = true;
@@ -1619,13 +1599,13 @@ var MobileMenuDrawer = (0, import_react16.memo)(
1619
1599
  }
1620
1600
  }
1621
1601
  }));
1622
- const bgColorStyle = (0, import_react16.useMemo)(() => {
1602
+ const bgColorStyle = (0, import_react17.useMemo)(() => {
1623
1603
  return { backgroundColor: theme.background.content };
1624
1604
  }, [theme.background.content]);
1625
- const handleIndicatorStyle = (0, import_react16.useMemo)(() => {
1605
+ const handleIndicatorStyle = (0, import_react17.useMemo)(() => {
1626
1606
  return { backgroundColor: theme.textMutedColor };
1627
1607
  }, [theme.textMutedColor]);
1628
- const contentContainerStyle2 = (0, import_react16.useMemo)(() => {
1608
+ const contentContainerStyle2 = (0, import_react17.useMemo)(() => {
1629
1609
  return { paddingBottom: insets.bottom };
1630
1610
  }, [insets.bottom]);
1631
1611
  return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
@@ -1660,7 +1640,7 @@ var MobileMenuDrawer = (0, import_react16.memo)(
1660
1640
  );
1661
1641
 
1662
1642
  // src/StorybookLogo.tsx
1663
- var import_react17 = require("react");
1643
+ var import_react18 = require("react");
1664
1644
  var import_react_native7 = require("react-native");
1665
1645
 
1666
1646
  // src/icon/DarkLogo.tsx
@@ -1801,11 +1781,11 @@ var WIDTH = 125;
1801
1781
  var HEIGHT = 25;
1802
1782
  var NoBrandLogo = ({ theme }) => theme.base === "light" ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(Logo, { height: HEIGHT, width: WIDTH }) : /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(DarkLogo, { height: HEIGHT, width: WIDTH });
1803
1783
  function isElement(value) {
1804
- return (0, import_react17.isValidElement)(value);
1784
+ return (0, import_react18.isValidElement)(value);
1805
1785
  }
1806
1786
  var BrandLogo = ({ theme }) => {
1807
1787
  const imageHasNoWidthOrHeight = typeof theme.brand.image === "object" && typeof theme.brand.image === "object" && "uri" in theme.brand.image && (!("height" in theme.brand.image) || !("width" in theme.brand.image));
1808
- (0, import_react17.useEffect)(() => {
1788
+ (0, import_react18.useEffect)(() => {
1809
1789
  if (imageHasNoWidthOrHeight) {
1810
1790
  console.warn(
1811
1791
  "STORYBOOK: When using a remote image as the brand logo, you must also set the width and height.\nFor example: brand: { image: { uri: 'https://sb.com/img.png', height: 25, width: 25}}"
@@ -1841,7 +1821,7 @@ var BrandLogo = ({ theme }) => {
1841
1821
  }
1842
1822
  };
1843
1823
  var BrandTitle = ({ theme }) => {
1844
- const brandTitleStyle = (0, import_react17.useMemo)(() => {
1824
+ const brandTitleStyle = (0, import_react18.useMemo)(() => {
1845
1825
  return {
1846
1826
  width: WIDTH,
1847
1827
  height: HEIGHT,
@@ -1865,8 +1845,8 @@ var BrandTitle = ({ theme }) => {
1865
1845
  }
1866
1846
  };
1867
1847
  var StorybookLogo = ({ theme }) => {
1868
- const image = (0, import_react17.useMemo)(() => theme.brand?.image, [theme.brand?.image]);
1869
- const title = (0, import_react17.useMemo)(() => theme.brand?.title, [theme.brand?.title]);
1848
+ const image = (0, import_react18.useMemo)(() => theme.brand?.image, [theme.brand?.image]);
1849
+ const title = (0, import_react18.useMemo)(() => theme.brand?.title, [theme.brand?.title]);
1870
1850
  if (image) {
1871
1851
  return /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(BrandLogo, { theme });
1872
1852
  } else if (title) {
@@ -1911,8 +1891,8 @@ var Layout = ({
1911
1891
  children
1912
1892
  }) => {
1913
1893
  const theme = (0, import_react_native_theming15.useTheme)();
1914
- const mobileMenuDrawerRef = (0, import_react18.useRef)(null);
1915
- const addonPanelRef = (0, import_react18.useRef)(null);
1894
+ const mobileMenuDrawerRef = (0, import_react19.useRef)(null);
1895
+ const addonPanelRef = (0, import_react19.useRef)(null);
1916
1896
  const insets = (0, import_react_native_safe_area_context3.useSafeAreaInsets)();
1917
1897
  const { isDesktop } = (0, import_react_native_ui_common7.useLayout)();
1918
1898
  const [desktopSidebarOpen, setDesktopSidebarOpen] = (0, import_react_native_ui_common7.useStoreBooleanState)(
@@ -1923,8 +1903,8 @@ var Layout = ({
1923
1903
  "desktopPanelState",
1924
1904
  true
1925
1905
  );
1926
- const [uiHidden, setUiHidden] = (0, import_react18.useState)(false);
1927
- (0, import_react18.useLayoutEffect)(() => {
1906
+ const [uiHidden, setUiHidden] = (0, import_react19.useState)(false);
1907
+ (0, import_react19.useLayoutEffect)(() => {
1928
1908
  setUiHidden(story?.parameters?.storybookUIVisibility === "hidden");
1929
1909
  }, [story?.parameters?.storybookUIVisibility]);
1930
1910
  const desktopSidebarStyle = (0, import_react_native_ui_common7.useStyle)(
@@ -1993,10 +1973,10 @@ var Layout = ({
1993
1973
  }),
1994
1974
  [theme.barTextColor]
1995
1975
  );
1996
- const openMobileMenu = (0, import_react18.useCallback)(() => {
1976
+ const openMobileMenu = (0, import_react19.useCallback)(() => {
1997
1977
  mobileMenuDrawerRef.current.setMobileMenuOpen(true);
1998
1978
  }, [mobileMenuDrawerRef]);
1999
- const setSelection = (0, import_react18.useCallback)(({ storyId: newStoryId }) => {
1979
+ const setSelection = (0, import_react19.useCallback)(({ storyId: newStoryId }) => {
2000
1980
  const channel = import_manager_api2.addons.getChannel();
2001
1981
  channel.emit(import_core_events.SET_CURRENT_STORY, { storyId: newStoryId });
2002
1982
  }, []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/react-native-ui",
3
- "version": "10.2.1",
3
+ "version": "10.2.2-alpha.1",
4
4
  "description": "ui components for react native storybook",
5
5
  "keywords": [
6
6
  "react",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@gorhom/portal": "^1.0.14",
41
- "@storybook/react": "^10.2.1",
42
- "@storybook/react-native-theming": "^10.2.1",
43
- "@storybook/react-native-ui-common": "^10.2.1",
44
- "fuse.js": "^7.1.0",
41
+ "@nozbe/microfuzz": "^1.0.0",
42
+ "@storybook/react": "^10.2.2",
43
+ "@storybook/react-native-theming": "^10.2.2-alpha.1",
44
+ "@storybook/react-native-ui-common": "^10.2.2-alpha.1",
45
45
  "polished": "^4.3.1"
46
46
  },
47
47
  "peerDependencies": {
@@ -60,5 +60,5 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "gitHead": "d4d35fdabf780ea51500824c9fe134b182e8c383"
63
+ "gitHead": "900db1ac2a226eb8206cc89b7142d1ea50272a9e"
64
64
  }
package/src/Search.tsx CHANGED
@@ -1,8 +1,7 @@
1
1
  import { BottomSheetTextInput, useBottomSheetInternal } from '@gorhom/bottom-sheet';
2
2
  import { styled } from '@storybook/react-native-theming';
3
- import type { IFuseOptions } from 'fuse.js';
4
- import Fuse from 'fuse.js';
5
- import React, { useCallback, useDeferredValue, useRef, useState } from 'react';
3
+ import { useFuzzySearchList } from '@nozbe/microfuzz/react';
4
+ import React, { useCallback, useDeferredValue, useMemo, useRef, useState } from 'react';
6
5
  import { Platform, TextInput, View } from 'react-native';
7
6
  import { CloseIcon } from './icon/CloseIcon';
8
7
  import { SearchIcon } from './icon/SearchIcon';
@@ -18,24 +17,11 @@ import {
18
17
  searchItem,
19
18
  } from '@storybook/react-native-ui-common';
20
19
 
21
- const DEFAULT_MAX_SEARCH_RESULTS = 50;
20
+ // Microfuzz highlight types
21
+ type HighlightRange = [number, number];
22
+ type HighlightRanges = HighlightRange[];
22
23
 
23
- const options = {
24
- shouldSort: true,
25
- tokenize: true,
26
- findAllMatches: true,
27
- includeScore: true,
28
- includeMatches: true,
29
- threshold: 0.2,
30
- location: 0,
31
- distance: 100,
32
- maxPatternLength: 32,
33
- minMatchCharLength: 1,
34
- keys: [
35
- { name: 'name', weight: 0.7 },
36
- { name: 'path', weight: 0.3 },
37
- ],
38
- } as IFuseOptions<SearchItem>;
24
+ const DEFAULT_MAX_SEARCH_RESULTS = 50;
39
25
 
40
26
  const SearchIconWrapper = styled.View({
41
27
  position: 'absolute',
@@ -111,7 +97,6 @@ export const Search = React.memo<{
111
97
  const [inputValue, setInputValue] = useState(initialQuery);
112
98
  const [isOpen, setIsOpen] = useState(false);
113
99
  const [allComponents, showAllComponents] = useState(false);
114
- // const { isMobile } = useLayout();
115
100
  const { scrollToSelectedNode } = useSelectedNode();
116
101
 
117
102
  const selectStory = useCallback(
@@ -133,7 +118,6 @@ export const Search = React.memo<{
133
118
  ({ item: result }) => {
134
119
  return {
135
120
  icon: result?.item?.type === 'component' ? 'component' : 'story',
136
- result,
137
121
  onPress: () => {
138
122
  if (result?.item?.type === 'story') {
139
123
  selectStory(result.item.id, result.item.refId);
@@ -144,7 +128,6 @@ export const Search = React.memo<{
144
128
  }
145
129
  },
146
130
  score: result.score,
147
- refIndex: result.refIndex,
148
131
  item: result.item,
149
132
  matches: result.matches,
150
133
  isHighlighted: false,
@@ -153,72 +136,103 @@ export const Search = React.memo<{
153
136
  [selectStory]
154
137
  );
155
138
 
156
- const makeFuse = useCallback(() => {
157
- const list = dataset.entries.reduce<SearchItem[]>((acc, [refId, { index }]) => {
139
+ // Defer dataset updates to prevent blocking during data changes
140
+ const deferredDataset = useDeferredValue(dataset);
141
+
142
+ // Build the search list - memoized
143
+ const searchList = useMemo(() => {
144
+ return deferredDataset.entries.reduce<SearchItem[]>((acc, [refId, { index }]) => {
158
145
  if (index) {
159
146
  acc.push(
160
147
  ...Object.values(index).map((item) => {
161
- return searchItem(item, dataset.hash[refId]);
148
+ return searchItem(item, deferredDataset.hash[refId]);
162
149
  })
163
150
  );
164
151
  }
165
152
  return acc;
166
153
  }, []);
167
- return new Fuse(list, options);
168
- }, [dataset]);
169
-
170
- const getResults = useCallback(
171
- (input: string) => {
172
- const fuse = makeFuse();
173
- if (!input) return [];
174
-
175
- let results = [];
176
- const resultIds: Set<string> = new Set();
177
- const distinctResults = (fuse.search(input) as SearchResult[]).filter(({ item }) => {
178
- if (
179
- !(item.type === 'component' || item.type === 'docs' || item.type === 'story') ||
180
- resultIds.has(item.parent)
181
- ) {
182
- return false;
183
- }
184
- resultIds.add(item.id);
185
- return true;
186
- });
154
+ }, [deferredDataset]);
155
+
156
+ // Defer query input to prevent blocking typing
157
+ const deferredQuery = useDeferredValue(inputValue);
158
+ const queryText = useMemo(() => (deferredQuery ? deferredQuery.trim() : ''), [deferredQuery]);
159
+
160
+ // getText function for microfuzz - memoized for performance
161
+ // Returns [name, path] - matches[0] will be name highlights, matches[1] will be path highlights
162
+ const getText = useCallback((item: SearchItem) => [item.name, item.path?.join(' ') ?? ''], []);
163
+
164
+ // Map microfuzz result to our SearchResult type (native format)
165
+ const mapResultItem = useCallback(
166
+ ({
167
+ item,
168
+ score,
169
+ matches,
170
+ }: {
171
+ item: SearchItem;
172
+ score: number | null;
173
+ matches: HighlightRanges[];
174
+ }): SearchResult => ({
175
+ item,
176
+ score,
177
+ matches: matches ?? [],
178
+ }),
179
+ []
180
+ );
181
+
182
+ // Use microfuzz's React hook with built-in memoization
183
+ const fuzzyResults = useFuzzySearchList({
184
+ list: searchList,
185
+ queryText,
186
+ getText,
187
+ mapResultItem,
188
+ });
189
+
190
+ // Process results: filter, deduplicate, and limit
191
+ const results = useMemo(() => {
192
+ if (!queryText) return [];
193
+
194
+ const maxResults = allComponents ? 1000 : DEFAULT_MAX_SEARCH_RESULTS;
195
+ const processedResults = [];
196
+ const resultIds = new Set<string>();
187
197
 
188
- if (distinctResults.length) {
189
- results = distinctResults.slice(0, allComponents ? 1000 : DEFAULT_MAX_SEARCH_RESULTS);
190
- if (distinctResults.length > DEFAULT_MAX_SEARCH_RESULTS && !allComponents) {
191
- results.push({
192
- showAll: () => showAllComponents(true),
193
- totalCount: distinctResults.length,
194
- moreCount: distinctResults.length - DEFAULT_MAX_SEARCH_RESULTS,
195
- });
196
- }
198
+ let totalDistinctCount = 0;
199
+
200
+ for (const result of fuzzyResults) {
201
+ const { item } = result;
202
+
203
+ // Skip invalid types or duplicates
204
+ if (
205
+ !(item.type === 'component' || item.type === 'docs' || item.type === 'story') ||
206
+ resultIds.has(item.parent)
207
+ ) {
208
+ continue;
197
209
  }
198
210
 
199
- const lastViewed = !input && getLastViewed();
200
- if (lastViewed && lastViewed.length) {
201
- results = lastViewed.reduce((acc, { storyId, refId }) => {
202
- const data = dataset.hash[refId];
203
- if (data && data.index && data.index[storyId]) {
204
- const story = data.index[storyId];
205
- const item = story.type === 'story' ? data.index[story.parent] : story;
206
- // prevent duplicates
207
- if (!acc.some((res) => res.item.refId === refId && res.item.id === item.id)) {
208
- acc.push({ item: searchItem(item, dataset.hash[refId]), matches: [], score: 0 });
209
- }
210
- }
211
- return acc;
212
- }, []);
211
+ resultIds.add(item.id);
212
+ totalDistinctCount++;
213
+
214
+ // Only add to results if we haven't reached the limit
215
+ if (processedResults.length < maxResults) {
216
+ processedResults.push(result);
213
217
  }
214
218
 
215
- return results;
216
- },
217
- [allComponents, dataset.hash, getLastViewed, makeFuse]
218
- );
219
- const deferredQuery = useDeferredValue(inputValue);
220
- const input = deferredQuery ? deferredQuery.trim() : '';
221
- const results = input ? getResults(input) : [];
219
+ // Early exit when showing all components and we have enough
220
+ if (allComponents && processedResults.length >= maxResults) {
221
+ break;
222
+ }
223
+ }
224
+
225
+ // Add "show all" option if there are more results than displayed
226
+ if (!allComponents && totalDistinctCount > DEFAULT_MAX_SEARCH_RESULTS) {
227
+ processedResults.push({
228
+ showAll: () => showAllComponents(true),
229
+ totalCount: totalDistinctCount,
230
+ moreCount: totalDistinctCount - DEFAULT_MAX_SEARCH_RESULTS,
231
+ });
232
+ }
233
+
234
+ return processedResults;
235
+ }, [queryText, fuzzyResults, allComponents]);
222
236
 
223
237
  return (
224
238
  <View style={{ flex: 1 }}>
@@ -251,7 +265,7 @@ export const Search = React.memo<{
251
265
  </SearchField>
252
266
 
253
267
  {children({
254
- query: input,
268
+ query: queryText,
255
269
  results,
256
270
  isBrowsing: !isOpen || !inputValue.length,
257
271
  closeMenu: () => {},
@@ -12,6 +12,9 @@ export default meta;
12
12
 
13
13
  type Story = StoryObj<typeof meta>;
14
14
 
15
+ // Microfuzz format: matches[0] = name highlights, matches[1] = path highlights
16
+ // Path is joined with spaces: "NestingExample Message bubble"
17
+ // "bubble" starts at index 23 (14 + 1 + 7 + 1 = 23)
15
18
  export const Default: Story = {
16
19
  args: {
17
20
  query: 'bubble',
@@ -35,48 +38,14 @@ export const Default: Story = {
35
38
  subtype: 'story',
36
39
  exportName: 'First',
37
40
  },
38
- refIndex: 46,
39
- matches: [
40
- {
41
- indices: [[0, 5]],
42
- value: 'bubble',
43
- key: 'path',
44
- refIndex: 2,
45
- },
46
- ],
41
+ // matches[0] = name highlights (none), matches[1] = path highlights
42
+ matches: [[], [[23, 28]]],
47
43
  score: 0.000020134092876783674,
48
44
  },
49
45
  ],
50
46
  getItemProps: () => ({
51
47
  icon: 'story',
52
- result: {
53
- item: {
54
- type: 'story',
55
- id: 'nestingexample-message-bubble--first',
56
- name: 'First',
57
- title: 'NestingExample/Message/bubble',
58
- importPath: './components/NestingExample/ChatMessageBubble.stories.tsx',
59
- tags: ['story'],
60
- depth: 3,
61
- parent: 'nestingexample-message-bubble',
62
- prepared: false,
63
- refId: 'storybook_internal',
64
- path: ['NestingExample', 'Message', 'bubble'],
65
- status: null,
66
- },
67
- refIndex: 46,
68
- matches: [
69
- {
70
- indices: [[0, 5]],
71
- value: 'bubble',
72
- key: 'path',
73
- refIndex: 2,
74
- },
75
- ],
76
- score: 0.000020134092876783674,
77
- },
78
48
  score: 0.000020134092876783674,
79
- refIndex: 46,
80
49
  item: {
81
50
  type: 'story',
82
51
  id: 'nestingexample-message-bubble--first',
@@ -93,14 +62,7 @@ export const Default: Story = {
93
62
  subtype: 'story',
94
63
  exportName: 'First',
95
64
  },
96
- matches: [
97
- {
98
- indices: [[0, 5]],
99
- value: 'bubble',
100
- key: 'path',
101
- refIndex: 2,
102
- },
103
- ],
65
+ matches: [[], [[23, 28]]],
104
66
  isHighlighted: false,
105
67
  onPress: () => {},
106
68
  }),
@@ -11,12 +11,15 @@ import {
11
11
  Button,
12
12
  } from '@storybook/react-native-ui-common';
13
13
 
14
- import { FuseResultMatch } from 'fuse.js';
15
14
  import { PressableProps, View } from 'react-native';
16
15
 
17
16
  import { ComponentIcon } from './icon/ComponentIcon';
18
17
  import { StoryIcon } from './icon/StoryIcon';
19
18
 
19
+ // Microfuzz highlight types
20
+ type HighlightRange = [number, number];
21
+ type HighlightRanges = HighlightRange[];
22
+
20
23
  const ResultsList = styled.View({
21
24
  margin: 0,
22
25
  padding: 0,
@@ -91,23 +94,29 @@ const RecentlyOpenedTitle = styled.View(({ theme }) => ({
91
94
  alignItems: 'center',
92
95
  }));
93
96
 
94
- const Highlight: FC<PropsWithChildren<{ match?: FuseResultMatch }>> = React.memo(
95
- function Highlight({ children, match }) {
96
- if (!match) return children;
97
- const { value, indices } = match;
97
+ // Highlight component using native microfuzz format
98
+ // ranges is an array of [start, end] tuples (end is inclusive in microfuzz)
99
+ const Highlight: FC<PropsWithChildren<{ text: string; ranges?: HighlightRanges }>> = React.memo(
100
+ function Highlight({ children, text, ranges }) {
101
+ if (!ranges || ranges.length === 0) return <Text>{children ?? text}</Text>;
98
102
 
99
- const { nodes: result } = indices.reduce<{ cursor: number; nodes: ReactNode[] }>(
103
+ const { nodes: result } = ranges.reduce<{ cursor: number; nodes: ReactNode[] }>(
100
104
  ({ cursor, nodes }, [start, end], index, { length }) => {
101
- nodes.push(<Text key={`text-${index}`}>{value.slice(cursor, start)}</Text>);
102
- nodes.push(<Mark key={`mark-${index}`}>{value.slice(start, end + 1)}</Mark>);
103
- if (index === length - 1) {
104
- nodes.push(<Text key={`last-${index}`}>{value.slice(end + 1)}</Text>);
105
+ // Add text before the highlight
106
+ if (cursor < start) {
107
+ nodes.push(<Text key={`text-${index}`}>{text.slice(cursor, start)}</Text>);
108
+ }
109
+ // Add highlighted text (end is inclusive in microfuzz)
110
+ nodes.push(<Mark key={`mark-${index}`}>{text.slice(start, end + 1)}</Mark>);
111
+ // Add remaining text after last highlight
112
+ if (index === length - 1 && end + 1 < text.length) {
113
+ nodes.push(<Text key={`last-${index}`}>{text.slice(end + 1)}</Text>);
105
114
  }
106
115
  return { cursor: end + 1, nodes };
107
116
  },
108
117
  { cursor: 0, nodes: [] }
109
118
  );
110
- return <Text key={`end-${match.key}`}>{result}</Text>;
119
+ return <Text>{result}</Text>;
111
120
  }
112
121
  );
113
122
 
@@ -145,8 +154,9 @@ const Result: FC<SearchResultProps> = React.memo(function Result({
145
154
  [onPress]
146
155
  );
147
156
 
148
- const nameMatch = matches.find((match: FuseResultMatch) => match.key === 'name');
149
- const pathMatches = matches.filter((match: FuseResultMatch) => match.key === 'path');
157
+ // matches[0] = name highlights, matches[1] = path highlights (as joined string)
158
+ const nameHighlights = matches?.[0];
159
+ const pathString = item.path?.join(' ') ?? '';
150
160
 
151
161
  return (
152
162
  <ResultRow {...props} onPress={press}>
@@ -156,25 +166,16 @@ const Result: FC<SearchResultProps> = React.memo(function Result({
156
166
  </IconWrapper>
157
167
  <ResultRowContent testID="search-result-item--label">
158
168
  <Title>
159
- <Highlight key="search-result-item--label-highlight" match={nameMatch}>
169
+ <Highlight text={item.name} ranges={nameHighlights}>
160
170
  {item.name}
161
171
  </Highlight>
162
172
  </Title>
163
173
  <Path>
164
- {item.path.map((group, index) => {
165
- const pathSeparator = index === item.path.length - 1 ? '' : '/';
166
- return (
167
- <View key={index} style={{ flexShrink: 1 }}>
168
- <PathText>
169
- <Highlight
170
- match={pathMatches.find((match: FuseResultMatch) => match.refIndex === index)}
171
- >
172
- {`${group}${pathSeparator}`}
173
- </Highlight>
174
- </PathText>
175
- </View>
176
- );
177
- })}
174
+ <PathText>
175
+ <Highlight text={pathString} ranges={matches?.[1]}>
176
+ {item.path?.join(' / ')}
177
+ </Highlight>
178
+ </PathText>
178
179
  </Path>
179
180
  </ResultRowContent>
180
181
  </ResultRow>