@storybook/react-native-ui 10.2.0 → 10.2.2-alpha.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/index.js +124 -144
- package/package.json +6 -6
- package/src/Search.tsx +91 -77
- package/src/SearchResults.stories.tsx +6 -44
- package/src/SearchResults.tsx +29 -28
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
|
|
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
|
|
736
|
-
var
|
|
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 =
|
|
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,
|
|
863
|
-
const [inputValue, setInputValue] = (0,
|
|
864
|
-
const [isOpen, setIsOpen] = (0,
|
|
865
|
-
const [allComponents, showAllComponents] = (0,
|
|
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,
|
|
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,
|
|
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
|
|
901
|
-
|
|
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,
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
1066
|
-
function Highlight2({ children,
|
|
1067
|
-
if (!
|
|
1068
|
-
const {
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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 }
|
|
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 =
|
|
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,
|
|
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
|
|
1114
|
-
const
|
|
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, {
|
|
1122
|
-
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Path9, { children:
|
|
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 =
|
|
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,
|
|
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 =
|
|
1191
|
+
var Swap = import_react13.default.memo(function Swap2({
|
|
1212
1192
|
children,
|
|
1213
1193
|
condition
|
|
1214
1194
|
}) {
|
|
1215
|
-
const [a, b] =
|
|
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,
|
|
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,
|
|
1217
|
+
return (0, import_react13.useMemo)(() => ({ hash, entries: Object.entries(hash) }), [hash]);
|
|
1238
1218
|
};
|
|
1239
|
-
var Sidebar =
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
1595
|
-
(0,
|
|
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,
|
|
1579
|
+
const menuBottomSheetRef = (0, import_react17.useRef)(null);
|
|
1600
1580
|
const { scrollToSelectedNode, scrollRef } = useSelectedNode();
|
|
1601
|
-
const shouldScrollOnOpen = (0,
|
|
1602
|
-
const handleSheetChange = (0,
|
|
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,
|
|
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,
|
|
1602
|
+
const bgColorStyle = (0, import_react17.useMemo)(() => {
|
|
1623
1603
|
return { backgroundColor: theme.background.content };
|
|
1624
1604
|
}, [theme.background.content]);
|
|
1625
|
-
const handleIndicatorStyle = (0,
|
|
1605
|
+
const handleIndicatorStyle = (0, import_react17.useMemo)(() => {
|
|
1626
1606
|
return { backgroundColor: theme.textMutedColor };
|
|
1627
1607
|
}, [theme.textMutedColor]);
|
|
1628
|
-
const contentContainerStyle2 = (0,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1869
|
-
const title = (0,
|
|
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,
|
|
1915
|
-
const addonPanelRef = (0,
|
|
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,
|
|
1927
|
-
(0,
|
|
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,
|
|
1976
|
+
const openMobileMenu = (0, import_react19.useCallback)(() => {
|
|
1997
1977
|
mobileMenuDrawerRef.current.setMobileMenuOpen(true);
|
|
1998
1978
|
}, [mobileMenuDrawerRef]);
|
|
1999
|
-
const setSelection = (0,
|
|
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.0",
|
|
3
|
+
"version": "10.2.2-alpha.0",
|
|
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
|
-
"@
|
|
42
|
-
"@storybook/react
|
|
43
|
-
"@storybook/react-native-
|
|
44
|
-
"
|
|
41
|
+
"@nozbe/microfuzz": "^1.0.0",
|
|
42
|
+
"@storybook/react": "^10.2.2",
|
|
43
|
+
"@storybook/react-native-theming": "^10.2.2-alpha.0",
|
|
44
|
+
"@storybook/react-native-ui-common": "^10.2.2-alpha.0",
|
|
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": "
|
|
63
|
+
"gitHead": "5e99a2f9de73aa3b88e6517f1016e933cac67c19"
|
|
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
|
|
4
|
-
import
|
|
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
|
-
|
|
20
|
+
// Microfuzz highlight types
|
|
21
|
+
type HighlightRange = [number, number];
|
|
22
|
+
type HighlightRanges = HighlightRange[];
|
|
22
23
|
|
|
23
|
-
const
|
|
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
|
-
|
|
157
|
-
|
|
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,
|
|
148
|
+
return searchItem(item, deferredDataset.hash[refId]);
|
|
162
149
|
})
|
|
163
150
|
);
|
|
164
151
|
}
|
|
165
152
|
return acc;
|
|
166
153
|
}, []);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}),
|
package/src/SearchResults.tsx
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 } =
|
|
103
|
+
const { nodes: result } = ranges.reduce<{ cursor: number; nodes: ReactNode[] }>(
|
|
100
104
|
({ cursor, nodes }, [start, end], index, { length }) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
-
|
|
149
|
-
const
|
|
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
|
|
169
|
+
<Highlight text={item.name} ranges={nameHighlights}>
|
|
160
170
|
{item.name}
|
|
161
171
|
</Highlight>
|
|
162
172
|
</Title>
|
|
163
173
|
<Path>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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>
|