<commit_before>/* eslint-env browser */

import { useStorybookApi } from '@storybook/api';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import Downshift, { DownshiftState, StateChangeOptions } from 'downshift';
import Fuse, { FuseOptions } from 'fuse.js';
import React, { useEffect, useMemo, useRef, useState, useCallback, FunctionComponent } from 'react';

import { DEFAULT_REF_ID } from './data';
import {
  CombinedDataset,
  SearchItem,
  SearchResult,
  DownshiftItem,
  SearchChildrenFn,
  Selection,
  isSearchResult,
  isExpandType,
  isClearType,
} from './types';
import { searchItem } from './utils';

const options = {
  shouldSort: true,
  tokenize: true,
  findAllMatches: true,
  includeScore: true,
  includeMatches: true,
  threshold: 0.2,
  location: 0,
  distance: 100,
  maxPatternLength: 32,
  minMatchCharLength: 1,
  keys: [
    { name: 'name', weight: 0.7 },
    { name: 'path', weight: 0.3 },
  ],
} as FuseOptions<SearchItem>;

const ScreenReaderLabel = styled.label({
  position: 'absolute',
  left: -10000,
  top: 'auto',
  width: 1,
  height: 1,
  overflow: 'hidden',
});

const SearchIcon = styled(Icons)(({ theme }) => ({
  width: 12,
  height: 12,
  position: 'absolute',
  top: 8,
  left: 10,
  zIndex: 1,
  pointerEvents: 'none',
  color: theme.color.mediumdark,
}));

const SearchField = styled.div(({ theme }) => ({
  display: 'flex',
  flexDirection: 'column',
  position: 'relative',
  '&:focus-within svg': {
    color: theme.color.defaultText,
  },
}));

const Input = styled.input(({ theme }) => ({
  height: 28,
  paddingLeft: 28,
  paddingRight: 28,
  border: `1px solid ${theme.appBorderColor}`,
  background: 'transparent',
  borderRadius: 28,
  fontSize: `${theme.typography.size.s1}px`,
  fontFamily: 'inherit',
  transition: 'all 150ms',
  color: theme.color.defaultText,
  '&:focus, &:active': {
    outline: 0,
    borderColor: theme.color.secondary,
    background: theme.barBg,
  },
  '&::placeholder': {
    color: theme.color.mediumdark,
  },
  '&:valid ~ code, &:focus ~ code': {
    display: 'none',
  },
  '&:invalid ~ svg': {
    display: 'none',
  },
  '&:valid ~ svg': {
    display: 'block',
  },
  '&::-ms-clear': {
    display: 'none',
  },
  '&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration': {
    display: 'none',
  },
}));

const FocusKey = styled.code(({ theme }) => ({
  position: 'absolute',
  top: 6,
  right: 12,
  width: 16,
  height: 16,
  zIndex: 1,
  lineHeight: '17px',
  textAlign: 'center',
  fontSize: '11px',
  background: 'rgba(0,0,0,0.1)',
  color: theme.color.mediumdark,
  borderRadius: 2,
  userSelect: 'none',
  pointerEvents: 'none',
}));

const ClearIcon = styled(Icons)(({ theme }) => ({
  width: 16,
  height: 16,
  padding: 4,
  position: 'absolute',
  top: 6,
  right: 12,
  zIndex: 1,
  background: 'rgba(0,0,0,0.1)',
  borderRadius: 16,
  color: theme.color.darker,
  cursor: 'pointer',
}));

const FocusContainer = styled.div({ outline: 0 });

export const Search: FunctionComponent<{
  children: SearchChildrenFn;
  dataset: CombinedDataset;
  isLoading?: boolean;
  lastViewed: Selection[];
  clearLastViewed: () => void;
  initialQuery?: string;
}> = ({ children, dataset, isLoading = false, lastViewed, clearLastViewed, initialQuery = '' }) => {
  const api = useStorybookApi();
  const inputRef = useRef<HTMLInputElement>(null);
  const [inputPlaceholder, setPlaceholder] = useState('Find components');
  const [allComponents, showAllComponents] = useState(false);

  const selectStory = useCallback(
    (id: string, refId: string) => {
      if (api) api.selectStory(id, undefined, { ref: refId !== DEFAULT_REF_ID && refId });
      inputRef.current.blur();
      showAllComponents(false);
    },
    [api, inputRef, showAllComponents, DEFAULT_REF_ID]
  );

  useEffect(() => {
    const focusSearch = (event: KeyboardEvent) => {
      if (isLoading || !inputRef.current) return;
      if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
      if (event.key === '/' && inputRef.current !== document.activeElement) {
        inputRef.current.focus();
        event.preventDefault();
      }
    };

    // Keyup prevents slashes from ending up in the input field when held down
    document.addEventListener('keyup', focusSearch);
    return () => document.removeEventListener('keyup', focusSearch);
  }, [inputRef, isLoading]);

  const list: SearchItem[] = useMemo(() => {
    return dataset.entries.reduce((acc: SearchItem[], [refId, { stories }]) => {
      if (stories) {
        acc.push(...Object.values(stories).map((item) => searchItem(item, dataset.hash[refId])));
      }
      return acc;
    }, []);
  }, [dataset]);

  const fuse = useMemo(() => new Fuse(list, options), [list]);

  const getResults = useCallback(
    (input: string) => {
      if (!input) return [];

      let results: DownshiftItem[] = [];
      const componentResults = (fuse.search(input) as SearchResult[]).filter(
        ({ item }) => item.isComponent
      );

      if (componentResults.length) {
        results = componentResults.slice(0, allComponents ? 100 : 10);
        if (componentResults.length > 10 && !allComponents) {
          results.push({
            showAll: () => showAllComponents(true),
            totalCount: componentResults.length,<commit_msg>Show 50 search results by default.<commit_after>/* eslint-env browser */

import { useStorybookApi } from '@storybook/api';
import { styled } from '@storybook/theming';
import { Icons } from '@storybook/components';
import Downshift, { DownshiftState, StateChangeOptions } from 'downshift';
import Fuse, { FuseOptions } from 'fuse.js';
import React, { useEffect, useMemo, useRef, useState, useCallback, FunctionComponent } from 'react';

import { DEFAULT_REF_ID } from './data';
import {
  CombinedDataset,
  SearchItem,
  SearchResult,
  DownshiftItem,
  SearchChildrenFn,
  Selection,
  isSearchResult,
  isExpandType,
  isClearType,
} from './types';
import { searchItem } from './utils';

const MAX_SEARCH_RESULTS = 50;

const options = {
  shouldSort: true,
  tokenize: true,
  findAllMatches: true,
  includeScore: true,
  includeMatches: true,
  threshold: 0.2,
  location: 0,
  distance: 100,
  maxPatternLength: 32,
  minMatchCharLength: 1,
  keys: [
    { name: 'name', weight: 0.7 },
    { name: 'path', weight: 0.3 },
  ],
} as FuseOptions<SearchItem>;

const ScreenReaderLabel = styled.label({
  position: 'absolute',
  left: -10000,
  top: 'auto',
  width: 1,
  height: 1,
  overflow: 'hidden',
});

const SearchIcon = styled(Icons)(({ theme }) => ({
  width: 12,
  height: 12,
  position: 'absolute',
  top: 8,
  left: 10,
  zIndex: 1,
  pointerEvents: 'none',
  color: theme.color.mediumdark,
}));

const SearchField = styled.div(({ theme }) => ({
  display: 'flex',
  flexDirection: 'column',
  position: 'relative',
  '&:focus-within svg': {
    color: theme.color.defaultText,
  },
}));

const Input = styled.input(({ theme }) => ({
  height: 28,
  paddingLeft: 28,
  paddingRight: 28,
  border: `1px solid ${theme.appBorderColor}`,
  background: 'transparent',
  borderRadius: 28,
  fontSize: `${theme.typography.size.s1}px`,
  fontFamily: 'inherit',
  transition: 'all 150ms',
  color: theme.color.defaultText,
  '&:focus, &:active': {
    outline: 0,
    borderColor: theme.color.secondary,
    background: theme.barBg,
  },
  '&::placeholder': {
    color: theme.color.mediumdark,
  },
  '&:valid ~ code, &:focus ~ code': {
    display: 'none',
  },
  '&:invalid ~ svg': {
    display: 'none',
  },
  '&:valid ~ svg': {
    display: 'block',
  },
  '&::-ms-clear': {
    display: 'none',
  },
  '&::-webkit-search-decoration, &::-webkit-search-cancel-button, &::-webkit-search-results-button, &::-webkit-search-results-decoration': {
    display: 'none',
  },
}));

const FocusKey = styled.code(({ theme }) => ({
  position: 'absolute',
  top: 6,
  right: 12,
  width: 16,
  height: 16,
  zIndex: 1,
  lineHeight: '17px',
  textAlign: 'center',
  fontSize: '11px',
  background: 'rgba(0,0,0,0.1)',
  color: theme.color.mediumdark,
  borderRadius: 2,
  userSelect: 'none',
  pointerEvents: 'none',
}));

const ClearIcon = styled(Icons)(({ theme }) => ({
  width: 16,
  height: 16,
  padding: 4,
  position: 'absolute',
  top: 6,
  right: 12,
  zIndex: 1,
  background: 'rgba(0,0,0,0.1)',
  borderRadius: 16,
  color: theme.color.darker,
  cursor: 'pointer',
}));

const FocusContainer = styled.div({ outline: 0 });

export const Search: FunctionComponent<{
  children: SearchChildrenFn;
  dataset: CombinedDataset;
  isLoading?: boolean;
  lastViewed: Selection[];
  clearLastViewed: () => void;
  initialQuery?: string;
}> = ({ children, dataset, isLoading = false, lastViewed, clearLastViewed, initialQuery = '' }) => {
  const api = useStorybookApi();
  const inputRef = useRef<HTMLInputElement>(null);
  const [inputPlaceholder, setPlaceholder] = useState('Find components');
  const [allComponents, showAllComponents] = useState(false);

  const selectStory = useCallback(
    (id: string, refId: string) => {
      if (api) api.selectStory(id, undefined, { ref: refId !== DEFAULT_REF_ID && refId });
      inputRef.current.blur();
      showAllComponents(false);
    },
    [api, inputRef, showAllComponents, DEFAULT_REF_ID]
  );

  useEffect(() => {
    const focusSearch = (event: KeyboardEvent) => {
      if (isLoading || !inputRef.current) return;
      if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
      if (event.key === '/' && inputRef.current !== document.activeElement) {
        inputRef.current.focus();
        event.preventDefault();
      }
    };

    // Keyup prevents slashes from ending up in the input field when held down
    document.addEventListener('keyup', focusSearch);
    return () => document.removeEventListener('keyup', focusSearch);
  }, [inputRef, isLoading]);

  const list: SearchItem[] = useMemo(() => {
    return dataset.entries.reduce((acc: SearchItem[], [refId, { stories }]) => {
      if (stories) {
        acc.push(...Object.values(stories).map((item) => searchItem(item, dataset.hash[refId])));
      }
      return acc;
    }, []);
  }, [dataset]);

  const fuse = useMemo(() => new Fuse(list, options), [list]);

  const getResults = useCallback(
    (input: string) => {
      if (!input) return [];

      let results: DownshiftItem[] = [];
      const componentResults = (fuse.search(input) as SearchResult[]).filter(
        ({ item }) => item.isComponent
      );

      if (componentResults.length) {
        results = componentResults.slice(0, allComponents ? 1000 : MAX_SEARCH_RESULTS);
        if (componentResults.length > MAX_SEARCH_RESULTS && !allComponents) {
          results.push({
            showAll: () => showAllComponents(true),
            totalCount: componentResults.length,
{'fraction_non_alphanumeric': 0.16593446824089328, 'fraction_numerical': 0.015650741350906095, 'mean_word_length': 3.6497872340425532, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2353052', 'n_tokens_mistral': 3744, 'n_tokens_neox': 3579, 'n_words': 847}