From 74939874966a847fc2cba2d7d8ec199e457ee262 Mon Sep 17 00:00:00 2001 From: winkerVSbecks Date: Wed, 6 Jan 2021 15:05:11 -0500 Subject: [PATCH 1/3] Addons catalog integrate back-end * Addon detail pages * Search * Wire up Popular and trending addons for home page * Linked tags --- .storybook/main.js | 7 + .storybook/use-addons-related-tags.mock.js | 32 + .storybook/use-addons-search.mock.js | 66 + gatsby-browser.js | 2 +- gatsby-config.js | 15 +- gatsby-node.js | 125 +- package.json | 2 + src/components/basics/Breadcrumb.js | 10 +- src/components/layout/addons/AddonItem.js | 28 +- .../layout/addons/AddonItem.stories.js | 30 +- .../layout/addons/AddonItemDetail.js | 52 +- .../layout/addons/AddonItemDetail.stories.js | 70 +- src/components/layout/addons/AddonsGrid.js | 9 +- .../layout/addons/AddonsGrid.stories.js | 59 +- src/components/layout/addons/AddonsLayout.js | 223 +- .../layout/addons/AddonsLayout.stories.js | 64 +- src/components/layout/addons/AddonsList.js | 20 +- .../layout/addons/AddonsSearchResults.js | 119 + src/components/layout/addons/VerifiedBadge.js | 3 +- .../AddonsCategoryScreen.js | 9 +- .../AddonsCategoryScreen.stories.js | 5 + .../AddonsDetailScreen/AddonsDetailScreen.js | 230 +- .../AddonsDetailScreen.stories.js | 87 +- .../AddonsHomeScreen/AddonsHomeScreen.js | 42 +- .../AddonsHomeScreen.stories.js | 2 +- .../AddonsTagScreen/AddonsTagScreen.js | 42 +- .../AddonsTagScreen.stories.js | 38 +- src/content/addons/addons.yml | 3803 ----------------- src/hooks/use-addons-related-tags.js | 32 + src/hooks/use-addons-search.js | 103 + src/pages/addons.js | 19 +- src/util/build-tag-links.js | 6 + src/util/fragments.js | 11 +- src/util/generate-breadcrumb.js | 11 + static/frameworks/logo-aurelia.svg | 63 + static/frameworks/logo-marionette.svg | 19 + yarn.lock | 333 +- 37 files changed, 1479 insertions(+), 4312 deletions(-) create mode 100644 .storybook/use-addons-related-tags.mock.js create mode 100644 .storybook/use-addons-search.mock.js create mode 100644 src/components/layout/addons/AddonsSearchResults.js delete mode 100644 src/content/addons/addons.yml create mode 100644 src/hooks/use-addons-related-tags.js create mode 100644 src/hooks/use-addons-search.js create mode 100644 src/util/build-tag-links.js create mode 100644 src/util/generate-breadcrumb.js create mode 100644 static/frameworks/logo-aurelia.svg create mode 100644 static/frameworks/logo-marionette.svg diff --git a/.storybook/main.js b/.storybook/main.js index b6f020f2..671271f9 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -60,6 +60,13 @@ module.exports = { ) ); + config.resolve.alias['../../../hooks/use-addons-search'] = require.resolve( + './use-addons-search.mock.js' + ); + config.resolve.alias['../../../hooks/use-addons-related-tags'] = require.resolve( + './use-addons-related-tags.mock.js' + ); + config.plugins.unshift( new webpack.DefinePlugin({ 'process.env.GATSBY_ALGOLIA_API_KEY': JSON.stringify(process.env.GATSBY_ALGOLIA_API_KEY), diff --git a/.storybook/use-addons-related-tags.mock.js b/.storybook/use-addons-related-tags.mock.js new file mode 100644 index 00000000..a8a3a813 --- /dev/null +++ b/.storybook/use-addons-related-tags.mock.js @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +const relatedTags = [ + { + link: '/notes', + name: 'πŸ—’ Notes', + }, + { + link: '/storybook', + name: 'πŸ“• Storybook', + }, + { + link: '/qa', + name: 'πŸ•΅οΈβ€β™€οΈ QA', + }, + { + link: '/prototype', + name: '✨ Prototype', + }, + { + link: '/testing', + name: 'βœ… Testing', + }, + { + link: '/deploy', + name: '☁️ Deploy', + }, +]; + +export const useAddonsRelatedTags = () => { + return relatedTags; +}; diff --git a/.storybook/use-addons-search.mock.js b/.storybook/use-addons-search.mock.js new file mode 100644 index 00000000..771818f9 --- /dev/null +++ b/.storybook/use-addons-search.mock.js @@ -0,0 +1,66 @@ +import { useState } from 'react'; + +let search = { query: '', isSearching: false, isSearchLoading: false, noResults: false }; + +export const useAddonsSearch = () => { + const [seachData] = useState(search); + return seachData; +}; + +export const UseAddonsSearchDecorator = (story, context) => { + search = { + query: context.parameters.isSearching ? 'layout' : '', + setQuery: () => {}, + isSearching: context.parameters.isSearching || false, + isSearchLoading: context.parameters.isSearchLoading || false, + results: context.parameters.noResults ? { search: [], relatedTags: [] } : mockResults, + }; + + return story(); +}; + +const mockResults = { + search: [ + { + id: 'storybook-addon-outline', + name: 'storybook-addon-outline', + displayName: 'Outline', + description: 'Outline all elements with CSS to help with layout placement and alignment', + icon: + 'https://user-images.githubusercontent.com/263385/101991674-48355c80-3c7c-11eb-9686-f684e755fcdd.png', + authors: [ + { + id: 'winkervsbecks', + avatarUrl: '//www.gravatar.com/avatar/dc3909557c0f933a066fe5faea796fdf?s=200', + name: 'winkervsbecks', + }, + ], + weeklyDownloads: 65, + repositoryUrl: 'https://github.com/chromaui/storybook-outline', + appearance: 'official', + verifiedCreator: null, + }, + ], + relatedTags: [ + { + name: 'docz', + link: '/addons/docz', + }, + { + name: 'md', + link: '/addons/md', + }, + { + name: 'storybook', + link: '/addons/jss', + }, + { + name: 'addon', + link: '/addons/end-to-end', + }, + { + name: 'storybookjs', + link: '/addons/hooks', + }, + ], +}; diff --git a/gatsby-browser.js b/gatsby-browser.js index 43069eca..c743e8b3 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,5 +1,5 @@ /* eslint-env browser */ -exports.onRouteUpdate = ({ location }) => { +exports.onRouteUpdate = ({ location, prevLocation }) => { if ( location.pathname.match(/iframe/) || !window.analytics || diff --git a/gatsby-config.js b/gatsby-config.js index 949b2299..0fd88b4b 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -8,10 +8,14 @@ require('dotenv').config({ module.exports = { siteMetadata, + flags: { + PRESERVE_WEBPACK_CACHE: true, + FAST_DEV: true, + QUERY_ON_DEMAND: true, + }, plugins: [ 'gatsby-plugin-react-helmet', 'gatsby-plugin-typescript', - 'gatsby-transformer-yaml', { resolve: `gatsby-source-filesystem`, options: { @@ -19,6 +23,15 @@ module.exports = { path: `${__dirname}/src/images`, }, }, + { + resolve: 'gatsby-source-graphql', + options: { + fieldName: `addons`, + url: `https://boring-heisenberg-43a6ed.netlify.app/`, + typeName: `ADDON`, + // refetchInterval: 60, + }, + }, 'gatsby-transformer-sharp', 'gatsby-plugin-sharp', { diff --git a/gatsby-node.js b/gatsby-node.js index beed8140..031a3db3 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,11 +1,15 @@ const fs = require('fs'); const path = require('path'); +const remark = require('remark'); +const remarkHTML = require('remark-html'); const { createFilePath } = require(`gatsby-source-filesystem`); const { toc: docsToc } = require('./src/content/docs/toc'); -const { categories: addonCategories } = require('./src/content/addons/categories'); const buildPathWithFramework = require('./src/util/build-path-with-framework'); +const buildTagLinks = require('./src/util/build-tag-links'); + +const processor = remark().use(remarkHTML); const githubDocsBaseUrl = 'https://github.com/storybookjs/storybook/tree/next'; const addStateToToc = (items, pathPrefix = '/docs') => @@ -64,32 +68,20 @@ exports.onCreatePage = ({ page, actions }) => { } }; -const addonsByCategoryQuery = Object.keys(addonCategories) - .map( - (categoryType) => ` - ${categoryType}Addons: allAddonsYaml(filter: { tags: { eq: "${addonCategories[categoryType].id}" } }) { - nodes { - id: name - name - displayName - description - icon - authors { - id: username - avatarUrl: gravatarUrl - name: username - } - weeklyDownloads - tags - repositoryUrl - addonUrl: npmUrl - appearance: verified - verifiedCreator - } - } - ` - ) - .join(''); +const addonDetail = ` + id: name + name + displayName + description + icon + authors { + id: username + avatarUrl: gravatarUrl + name: username + } + weeklyDownloads + appearance: verified + verifiedCreator`; exports.createPages = ({ actions, graphql }) => { const { createRedirect, createPage } = actions; @@ -121,23 +113,61 @@ exports.createPages = ({ actions, graphql }) => { } } } + addons { + addonPages: top(sort: monthlyDownloads) { + ${addonDetail} + tags { + name + displayName + description + icon + } + compatibility { + name + displayName + icon + } + status + readme + publishedAt + repositoryUrl + homepageUrl + } + tagPages: tags(isCategory: false) { + name + displayName + description + icon + addons: top(sort: monthlyDownloads) { + ${addonDetail} + } + } + categoryPages: tags(isCategory: true) { + name + displayName + description + icon + addons: top(sort: monthlyDownloads) { + ${addonDetail} + } + } + } site { siteMetadata { coreFrameworks communityFrameworks } } - ${addonsByCategoryQuery} } `).then( ({ data: { docsPages: { edges: docsPagesEdges }, releasePages: { edges: releasePagesEdges }, + addons: { addonPages, tagPages, categoryPages }, site: { siteMetadata: { coreFrameworks, communityFrameworks }, }, - ...addonsByCategory }, }) => { const sortedReleases = releasePagesEdges.sort( @@ -258,22 +288,43 @@ exports.createPages = ({ actions, graphql }) => { }); } - const createAddonCategoryPage = (category, addons) => + addonPages.forEach((addon) => { + createPage({ + path: `/addons/${addon.name}`, + component: path.resolve( + `./src/components/screens/AddonsDetailScreen/AddonsDetailScreen.js` + ), + context: { + ...addon, + tags: buildTagLinks(addon.tags), + readme: processor.processSync(addon.readme).toString(), + }, + }); + }); + + tagPages.forEach((tag) => { createPage({ - path: category.path, + path: `/addons/${tag.name}`, + component: path.resolve(`./src/components/screens/AddonsTagScreen/AddonsTagScreen.js`), + context: { + tag, + name: tag.name, + }, + }); + }); + + categoryPages.forEach((category) => { + createPage({ + path: `/addons/${category.name}`, component: path.resolve( `./src/components/screens/AddonsCategoryScreen/AddonsCategoryScreen.js` ), context: { - category: category.name, + category: category.displayName, description: category.description, - addons, + addons: category.addons, }, }); - - Object.keys(addonCategories).forEach((categoryType) => { - const addons = addonsByCategory[`${categoryType}Addons`].nodes; - createAddonCategoryPage(addonCategories[categoryType], addons); }); resolve(); diff --git a/package.json b/package.json index ecebd23a..293f7198 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "gatsby-remark-copy-linked-files": "^2.3.11", "gatsby-remark-link-rewrite": "^0.2.1", "gatsby-source-filesystem": "^2.3.18", + "gatsby-source-graphql": "^2.11.0", "gatsby-transformer-sharp": "^2.5.10", "gatsby-transformer-yaml": "^2.7.0", "global": "^4.4.0", @@ -56,6 +57,7 @@ "react-helmet": "^5.2.1", "react-lazyload": "^2.6.5", "recompose": "^0.30.0", + "remark-html": "^13.0.1", "request": "^2.88.2", "semver": "^7.3.2", "styled-components": "^4.4.1", diff --git a/src/components/basics/Breadcrumb.js b/src/components/basics/Breadcrumb.js index 133d07ee..4e8ca7d8 100644 --- a/src/components/basics/Breadcrumb.js +++ b/src/components/basics/Breadcrumb.js @@ -3,13 +3,19 @@ import styled from 'styled-components'; import { styles, Icon } from '@storybook/design-system'; import GatsbyLink from './GatsbyLink'; -const { typography } = styles; +const { breakpoint, typography } = styles; const BreadcrumbLink = styled(GatsbyLink)` font-size: ${typography.size.s2}px; line-height: ${typography.size.m2}px; font-weight: ${typography.weight.bold}; - margin-bottom: 8px; + margin-top: 16px; + margin-bottom: 16px; + + @media (min-width: ${breakpoint * 1.333}px) { + margin-top: 0; + margin-bottom: 8px; + } `; export const Breadcrumb = ({ children, ...props }) => ( diff --git a/src/components/layout/addons/AddonItem.js b/src/components/layout/addons/AddonItem.js index 9fa54e80..746456b9 100644 --- a/src/components/layout/addons/AddonItem.js +++ b/src/components/layout/addons/AddonItem.js @@ -181,21 +181,24 @@ export const AddonItem = ({ displayName, description, weeklyDownloads, - addonUrl, authors, orientation, appearance, isLoading, verifiedCreator, + from, ...props }) => ( - {/* TODO: as={GatsbyLinkWrapper} to={addonUrl} when detail page is enabled */} - {!isLoading && addonUrl && ( - + {!isLoading && ( + )} - +
<span>{isLoading ? 'loading' : displayName || name}</span> @@ -233,10 +236,10 @@ export const AddonItem = ({ AddonItem.propTypes = { orientation: PropTypes.oneOf(['vertical', 'horizontal']), appearance: PropTypes.oneOf(['official', 'integrators', 'community']), - icon: PropTypes.node, - name: PropTypes.node, - displayName: PropTypes.node, - description: PropTypes.node, + icon: PropTypes.string, + name: PropTypes.string, + displayName: PropTypes.string, + description: PropTypes.string, weeklyDownloads: PropTypes.number, authors: PropTypes.arrayOf( PropTypes.shape({ @@ -245,19 +248,20 @@ AddonItem.propTypes = { avatarUrl: PropTypes.string, }) ), - addonUrl: PropTypes.string, isLoading: PropTypes.bool, verifiedCreator: PropTypes.string, + from: PropTypes.shape({ + title: PropTypes.string, + link: PropTypes.string, + }), }; AddonItem.defaultProps = { orientation: 'horizontal', appearance: 'community', - icon: '', weeklyDownloads: 0, authors: [], isLoading: false, - addonUrl: '#', name: '', description: '', verifiedCreator: '', diff --git a/src/components/layout/addons/AddonItem.stories.js b/src/components/layout/addons/AddonItem.stories.js index 7ee738b7..9567756d 100644 --- a/src/components/layout/addons/AddonItem.stories.js +++ b/src/components/layout/addons/AddonItem.stories.js @@ -52,15 +52,9 @@ const authors = [ const Template = (args) => ( <Wrapper> <h2>Horizontal</h2> - <AddonItem authors={authors} addonUrl="/addons/controls" {...args} /> + <AddonItem authors={authors} {...args} /> <h2>Vertical</h2> - <AddonItem - orientation="vertical" - authors={authors} - addonUrl="/addons/controls" - style={{ width: 300 }} - {...args} - /> + <AddonItem orientation="vertical" authors={authors} style={{ width: 300 }} {...args} /> </Wrapper> ); @@ -68,7 +62,7 @@ export const OfficialStorybook = Template.bind({}); OfficialStorybook.args = { appearance: 'official', icon: ControlsSVG, - name: 'Controls', + displayName: 'Controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 17143, }; @@ -77,7 +71,7 @@ export const OfficialIntegrator = Template.bind({}); OfficialIntegrator.args = { appearance: 'integrators', icon: ContrastPNG, - name: 'Contrast', + displayName: 'Contrast', description: 'Embed Contrast handoff tool in a storybook panel', weeklyDownloads: 17143, verifiedCreator: 'Contrast', @@ -87,7 +81,7 @@ export const Community = Template.bind({}); Community.args = { icon: ViewportSVG, appearance: 'community', - name: 'Mobile UX Hints', + displayName: 'Mobile UX Hints', description: 'Suggestions on how to tweak the HTML and CSS of your components to be more mobile-friendly.', weeklyDownloads: 12253, @@ -95,7 +89,15 @@ Community.args = { export const WithoutImage = Template.bind({}); WithoutImage.args = { - name: 'Controls', + displayName: 'Controls', + description: 'Interact with component inputs dynamically in the Storybook UI', + weeklyDownloads: 238, +}; + +export const WithoutDisplayName = Template.bind({}); +WithoutDisplayName.args = { + displayName: null, + name: '@storybook/addon-controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 238, }; @@ -125,8 +127,8 @@ StatVariations.args = { orientation: 'horizontal', appearance: 'official', icon: ControlsSVG, - name: 'Controls', + name: '@storybook/addon-controls', + displayName: 'Controls', description: 'Interact with component inputs dynamically in the Storybook UI', authors, - addonUrl: '/addons/controls', }; diff --git a/src/components/layout/addons/AddonItemDetail.js b/src/components/layout/addons/AddonItemDetail.js index 26896680..7aecf546 100644 --- a/src/components/layout/addons/AddonItemDetail.js +++ b/src/components/layout/addons/AddonItemDetail.js @@ -199,18 +199,17 @@ export const AddonItemDetail = ({ displayName, description, weeklyDownloads, - addonUrl, appearance, status, isLoading, - packageName, verifiedCreator, - updated, + publishedAt, + npmUrl, ...props }) => ( <AddonItemWrapper {...props}> <AddonInfo> - <Image isLoading={isLoading} src={icon === '' ? customSVG : icon} /> + <Image isLoading={isLoading} src={icon || customSVG} /> <div> <Title isLoading={isLoading}> <span>{isLoading ? 'loading' : displayName || name}</span> @@ -223,18 +222,20 @@ export const AddonItemDetail = ({ <span>{isLoading ? 'loading description of addon' : description}</span> </Description> <Instructions status={status}> - <ClipboardCode code={`npx install ${packageName}`} /> - <Update> - <Link href={updated.url}> - Last updated {formatDistanceToNow(new Date(updated.date), { addSuffix: true })} - </Link> - {status === 'essential' && ( - <> - {' β€’ '} - <Link href={updated.url}>Pre-installed with Storybook</Link> - </> - )} - </Update> + <ClipboardCode code={`npx install ${name}`} /> + {publishedAt && ( + <Update> + <Link href={npmUrl} target="_blank" rel="noopener nofollow noreferrer"> + Last updated {formatDistanceToNow(new Date(publishedAt), { addSuffix: true })} + </Link> + {status === 'essential' && ( + <> + {' β€’ '} + <Link href={npmUrl}>Pre-installed with Storybook</Link> + </> + )} + </Update> + )} </Instructions> </div> </AddonInfo> @@ -264,29 +265,24 @@ export const AddonItemDetail = ({ AddonItemDetail.propTypes = { appearance: PropTypes.oneOf(['official', 'integrators', 'community']), status: PropTypes.oneOf(['default', 'essential', 'deprecated']), - icon: PropTypes.node, - name: PropTypes.node, - displayName: PropTypes.node, - description: PropTypes.node, + icon: PropTypes.string, + name: PropTypes.string, + displayName: PropTypes.string, + description: PropTypes.string, weeklyDownloads: PropTypes.number, - addonUrl: PropTypes.string, isLoading: PropTypes.bool, verifiedCreator: PropTypes.string, - updated: PropTypes.shape({ - date: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - }).isRequired, - packageName: PropTypes.string.isRequired, + publishedAt: PropTypes.number, + npmUrl: PropTypes.string, }; AddonItemDetail.defaultProps = { appearance: 'community', status: 'default', - icon: '', weeklyDownloads: 0, isLoading: false, - addonUrl: '#', name: '', description: '', verifiedCreator: '', + npmUrl: '', }; diff --git a/src/components/layout/addons/AddonItemDetail.stories.js b/src/components/layout/addons/AddonItemDetail.stories.js index 06a67ee6..d3a9eb18 100644 --- a/src/components/layout/addons/AddonItemDetail.stories.js +++ b/src/components/layout/addons/AddonItemDetail.stories.js @@ -13,22 +13,22 @@ export default { const authors = [ { id: '1', - name: 'Dominic Nguyen', + displayName: 'Dominic Nguyen', avatarUrl: 'https://avatars2.githubusercontent.com/u/263385', }, { id: '2', - name: 'Tom Coleman', + displayName: 'Tom Coleman', avatarUrl: 'https://avatars2.githubusercontent.com/u/132554', }, { id: '3', - name: 'Zoltan Olah', + displayName: 'Zoltan Olah', avatarUrl: 'https://avatars0.githubusercontent.com/u/81672', }, { id: '4', - name: 'Tim Hingston', + displayName: 'Tim Hingston', avatarUrl: 'https://avatars3.githubusercontent.com/u/1831709', }, ]; @@ -42,89 +42,77 @@ const today = new Date(); const offsetDate = (offset) => { const d = new Date(); d.setDate(today.getDate() + (offset + 1)); - return d.toISOString(); + return Number(d); }; export const OfficialStorybook = Template.bind({}); OfficialStorybook.args = { appearance: 'official', icon: ControlsSVG, - name: 'Controls', + name: '@storybook/addon-controls', + displayName: 'Controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 17143, - updated: { - date: offsetDate(-35), - url: 'https://npmjs.org/', - }, - packageName: '@storybook/addon-controls', + publishedAt: offsetDate(-35), + npmUrl: 'https://npmjs.org/', }; export const OfficialIntegrator = Template.bind({}); OfficialIntegrator.args = { appearance: 'integrators', icon: ContrastPNG, - name: 'Contrast', + name: 'storybook-contrast', + displayName: 'Contrast', description: 'Embed Contrast handoff tool in a storybook panel', weeklyDownloads: 17143, - updated: { - date: offsetDate(-5), - url: 'https://npmjs.org/', - }, - packageName: 'storybook-contrast', + publishedAt: offsetDate(-5), + npmUrl: 'https://npmjs.org/', }; export const Community = Template.bind({}); Community.args = { icon: ViewportSVG, appearance: 'community', - name: 'Mobile UX Hints', + displayName: 'Mobile UX Hints', + name: '@storybook/addon-viewport', description: 'Suggestions on how to tweak the HTML and CSS of your components to be more mobile-friendly.', weeklyDownloads: 12253, - updated: { - date: offsetDate(-365), - url: 'https://npmjs.org/', - }, - packageName: '@storybook/addon-viewport', + publishedAt: offsetDate(-365), + npmUrl: 'https://npmjs.org/', }; export const WithoutImage = Template.bind({}); WithoutImage.args = { - name: 'Controls', + name: '@storybook/addon-controls', + displayName: 'Controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 238, - updated: { - date: offsetDate(-72), - url: 'https://npmjs.org/', - }, - packageName: '@storybook/addon-controls', + publishedAt: offsetDate(-72), + npmUrl: 'https://npmjs.org/', }; export const Essential = Template.bind({}); Essential.args = { - name: 'Controls', + displayName: 'Controls', description: 'Interact with component inputs dynamically in the Storybook UI', + name: '@storybook/addon-controls', weeklyDownloads: 238, - updated: { - date: offsetDate(-32), - url: 'https://npmjs.org/', - }, + publishedAt: offsetDate(-32), + npmUrl: 'https://npmjs.org/', status: 'essential', appearance: 'official', - packageName: '@storybook/addon-controls', }; export const Deprecated = Template.bind({}); Deprecated.args = { appearance: 'official', icon: ControlsSVG, - name: 'Controls', + displayName: 'Controls', + name: '@storybook/addon-controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 17143, status: 'deprecated', - updated: { - date: offsetDate(-730), - url: 'https://npmjs.org/', - }, - packageName: '@storybook/addon-controls', + publishedAt: offsetDate(-730), + npmUrl: 'https://npmjs.org/', }; diff --git a/src/components/layout/addons/AddonsGrid.js b/src/components/layout/addons/AddonsGrid.js index 069ee138..76e8030c 100644 --- a/src/components/layout/addons/AddonsGrid.js +++ b/src/components/layout/addons/AddonsGrid.js @@ -30,7 +30,7 @@ const SectionHeader = styled.div` margin-bottom: ${spacing.padding.medium}px; `; -export const AddonsGrid = ({ title, actions, addonItems, ...props }) => ( +export const AddonsGrid = ({ title, actions, addonItems, from, ...props }) => ( <section> <SectionHeader> <Title>{title} @@ -38,17 +38,22 @@ export const AddonsGrid = ({ title, actions, addonItems, ...props }) => ( {addonItems.map((addon) => ( - + ))} ); +/* eslint-disable react/require-default-props */ AddonsGrid.propTypes = { title: PropTypes.string.isRequired, addonItems: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, ...AddonItem.propTypes }) ), + from: PropTypes.shape({ + title: PropTypes.string, + link: PropTypes.string, + }), }; AddonsGrid.defaultProps = { diff --git a/src/components/layout/addons/AddonsGrid.stories.js b/src/components/layout/addons/AddonsGrid.stories.js index ecf7eb63..97fdaef8 100644 --- a/src/components/layout/addons/AddonsGrid.stories.js +++ b/src/components/layout/addons/AddonsGrid.stories.js @@ -50,141 +50,140 @@ export const addonItemsData = [ id: '0', appearance: 'official', icon: ControlsSVG, - name: 'Controls', + displayName: 'Controls', + name: '@storybook/addon-controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 17143, authors, - addonUrl: '/addons/controls', }, { id: '1', icon: ViewportSVG, appearance: 'community', - name: 'Mobile UX Hints', + displayName: 'Mobile UX Hints', + name: 'storybook-mobile', description: 'Suggestions on how to tweak the HTML and CSS of your components to be more mobile-friendly.', weeklyDownloads: 12253, authors, - addonUrl: '/addons/mobile', }, { id: '2', appearance: 'integrators', verifiedCreator: 'Contrast', icon: ContrastPNG, - name: 'Contrast', + displayName: 'Contrast', + name: 'storybook-contrast', description: 'Embed Contrast handoff tool in a storybook panel', weeklyDownloads: 3892, authors, - addonUrl: '/addons/contrast', }, { id: '3', appearance: 'official', icon: AccessibilitySVG, - name: 'Accessibility', + displayName: 'Accessibility', + name: '@storybook/addon-a11y', description: 'Test component compliance with web accessibility standards', weeklyDownloads: 923, authors: authors.slice(2, 3), - addonUrl: '/addons/accessibility', }, { id: '4', appearance: 'official', icon: ActionsSVG, - name: 'Actions', + displayName: 'Actions', + name: '@storybook/addon-actions', description: 'Get UI feedback when an action is performed on an interactive element', weeklyDownloads: 8374, authors: authors.slice(0, 1), - addonUrl: '/addons/actions', }, { id: '5', appearance: 'official', icon: BackgroundsSVG, - name: 'Backgrounds', + displayName: 'Backgrounds', + name: '@storybook/addon-backgrounds', description: 'Switch backgrounds to view components in different settings', weeklyDownloads: 234, authors: authors.slice(0, 2), - addonUrl: '/addons/backgrounds', }, { id: '6', appearance: 'official', icon: ConsoleSVG, - name: 'Console', + displayName: 'Console', + name: '@storybook/addon-console', description: 'Show console output like logs, errors, and warnings in the Storybook', weeklyDownloads: 343, authors: authors.slice(1, 2), - addonUrl: '/addons/console', }, { id: '7', icon: CustomSVG, - name: 'Controls', + displayName: 'Controls', + name: '@storybook/addon-controls', description: 'Interact with component inputs dynamically in the Storybook UI', weeklyDownloads: 12, authors: authors.slice(1, 2), - addonUrl: '/addons/controls', }, { id: '8', appearance: 'integrators', verifiedCreator: 'InVision', icon: DocsSVG, - name: 'Docs', + displayName: 'Docs', description: 'Document component usage and properties in Markdown', weeklyDownloads: 72936, authors: authors.slice(1, 3), - addonUrl: '/addons/docs', }, { id: '9', icon: LinksSVG, - name: 'Links', + displayName: 'Links', + name: '@storybook/addon-links', description: 'Link stories together to build demos and prototypes with your UI components', weeklyDownloads: 1734143, authors, - addonUrl: '/addons/links', }, { id: '10', icon: OutlineSVG, - name: 'Outline', + displayName: 'Outline', + name: 'storybook-addon-outline', description: 'Outline all elements with CSS to help with layout placement and alignment', weeklyDownloads: 294, authors: authors.slice(0, 1), - addonUrl: '/addons/outline', }, { id: '11', icon: SourceSVG, - name: 'Source', + displayName: 'Source', + name: '@storybook/addon-storysource', description: 'View a story’s source code to see how it works and paste into your app.', weeklyDownloads: 3827, authors, - addonUrl: '/addons/source', }, { id: '12', appearance: 'integrators', verifiedCreator: 'Someone', icon: StoryshotsSVG, - name: 'Storyshots', + displayName: 'Storyshots', + name: '@storybook/addon-storyshots', description: 'Take a code snapshot of every story automatically with Jest', weeklyDownloads: 5643, authors, - addonUrl: '/addons/storyshots', }, { id: '13', appearance: 'official', icon: ToolbarsSVG, - name: 'Toolbars', + displayName: 'Toolbars', + name: '@storybook/addon-toolbars', description: 'Create your own toolbar items that control story rendering', weeklyDownloads: 8473, authors, - addonUrl: '/addons/toolbars', }, ]; @@ -205,8 +204,8 @@ WithActions.args = { selectedIndex={0} onSelectIndex={() => {}} titles={[ - { name: 'Month', tooltip: 'Month' }, - { name: 'Year', tooltip: 'Year' }, + { title: 'Month', tooltip: 'Month' }, + { title: 'Year', tooltip: 'Year' }, ]} /> ), diff --git a/src/components/layout/addons/AddonsLayout.js b/src/components/layout/addons/AddonsLayout.js index d63173d5..dfcdb8ab 100644 --- a/src/components/layout/addons/AddonsLayout.js +++ b/src/components/layout/addons/AddonsLayout.js @@ -1,14 +1,13 @@ -import React from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import pluralize from 'pluralize'; -import { Input, TableOfContents, global, styles, TagList, TagLink } from '@storybook/design-system'; +import { Input, Icon, TableOfContents, global, styles } from '@storybook/design-system'; import GatsbyLinkWrapper from '../../basics/GatsbyLinkWrapper'; import { AddonsLearn } from './AddonsLearn'; -import { AddonsList } from './AddonsList'; -import { AddonsAside, AddonsAsideContainer } from './AddonsAsideLayout'; import { AddonsSubheading } from './AddonsSubheading'; +import { AddonsSearchSummary, AddonsSearchResults } from './AddonsSearchResults'; import { toc as addonsToc } from '../../../content/addons/categories'; +import { useAddonsSearch } from '../../../hooks/use-addons-search'; const { breakpoint, spacing, color, pageMargins, typography } = styles; const { GlobalStyle } = global; @@ -57,98 +56,94 @@ Sidebar.propTypes = { hideSidebar: PropTypes.bool.isRequired, }; -// TODO: use after preview release -// const ToCContent = styled.div` -// /* Hide ToC on mobile, the primary navigation is search */ -// display: none; - -// ${(props) => -// props.hideToC -// ? ` -// display: none; -// ` -// : ` -// @media (min-width: ${breakpoint * 1.333}px) { -// display: block; -// margin-top: 1.5rem; -// } -// `} -// `; - -// TODO: remove after preview release const ToCContent = styled.div` - margin-top: 1.5rem; - - .hide-on-mobile { - display: none; - } - - @media (min-width: ${breakpoint * 1.333}px) { - margin-top: 0.5rem; - - .hide-on-mobile { - display: block; - } - } - - @media (max-width: ${breakpoint * 1.333}px) { - ul { - display: flex; - flex-wrap: wrap; - } - - li { - padding-top: 0; - margin-right: ${spacing.padding.medium}px; - margin-bottom: ${spacing.padding.small}px; - } - } + /* Hide ToC on mobile, the primary navigation is search */ + display: none; + + ${(props) => + props.hideToC + ? ` + display: none; + ` + : ` + @media (min-width: ${breakpoint * 1.333}px) { + display: block; + margin-top: 1.5rem; + } + `} `; ToCContent.propTypes = { hideToC: PropTypes.bool.isRequired, }; -const StyledAddonsList = styled(AddonsList)` +const SearchInputContainer = styled.div` flex: 1 1 auto; + position: relative; + + @media (min-width: ${breakpoint * 1.333}px) { + max-width: 220px; + margin-right: ${(props) => (props.searchLayout ? 40 : 0)}px; + } `; -const RelatedTagsList = styled(TagList)` - margin-bottom: 48px; +const ClearButton = styled.button` + background-color: ${color.border}; + background-repeat: no-repeat; + border: none; + border-radius: 100%; + overflow: hidden; + outline: none; + + font-size: 10px; + line-height: 1; + position: absolute; + top: 11px; + right: 16px; + padding: 4px; + + &:focus { + box-shadow: ${color.secondary} 0 0 0 1px inset; + } + + svg { + display: block; + margin-right: 0; + height: 1em; + width: 1em; + color: ${color.dark}; + } `; const SearchInput = styled(Input)` - flex: 1 1 auto; + width: 100%; #addons-search { font-size: ${typography.size.s2}px; padding-left: 40px; padding-top: 12px; padding-bottom: 12px; + + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; + } } svg { left: 16px; font-size: ${typography.size.s2}px; } - - @media (min-width: ${breakpoint * 1.333}px) { - max-width: 220px; - margin-right: ${(props) => (props.searchLayout ? 40 : 0)}px; - } `; const Searchbar = styled.div` display: flex; -`; - -const SearchSummary = styled.div` - font-size: ${typography.weight.black}; - line-height: 28px; - color: ${color.darkest}; + align-items: center; `; const CategoriesHeading = styled(AddonsSubheading)` + margin-top: ${spacing.padding.medium}px; margin-bottom: ${spacing.padding.medium}px; `; @@ -156,73 +151,65 @@ export const SEARCH_INPUT_ID = 'addons-search'; const sidebarItems = addonsToc.map((item) => ({ ...item, LinkWrapper: GatsbyLinkWrapper })); -export const AddonsLayout = ({ - children, - data, - hideSidebar, - searchQuery, - searchResults, - currentPath, - ...props -}) => { - // TODO: connect to back-end - const searching = !!(searchQuery && searchQuery !== ''); +export const AddonsLayout = ({ children, data, hideSidebar, currentPath, ...props }) => { + const { query, setQuery, isSearching, isSearchLoading, results } = useAddonsSearch(); + const inputRef = useRef(null); return ( <> - - - {/* TODO: enable after preview release */} - {/* - {}} - /> - {searching && searchResults.addons && ( - - {pluralize('addons', searchResults.addons.length, true)} - + + + + + { + setQuery(e.target.value); + }} + /> + {query !== '' && ( + { + setQuery(''); + inputRef.current.focus(); + }} + > + + + )} + + {isSearching && results.search && ( + )} - */} + {({ menu }) => ( - + Categories {menu} - {/* TODO: remove after preview release */} -
- - -
+ +
)}
- {searching ? ( - - - - Related tags - ( - - {tag.name} - - ))} - isLoading={searchResults.relatedTags?.length === 0} - /> - - + {isSearching ? ( + ) : ( {children} )} diff --git a/src/components/layout/addons/AddonsLayout.stories.js b/src/components/layout/addons/AddonsLayout.stories.js index f17c82cc..1d202fb3 100644 --- a/src/components/layout/addons/AddonsLayout.stories.js +++ b/src/components/layout/addons/AddonsLayout.stories.js @@ -1,7 +1,7 @@ import React from 'react'; import seedrandom from 'seedrandom'; import { AddonsLayout } from './AddonsLayout'; -import { addonItemsData } from './AddonsGrid.stories'; +import { UseAddonsSearchDecorator } from '../../../../.storybook/use-addons-search.mock'; seedrandom('chromatic testing', { global: true }); @@ -9,6 +9,7 @@ export default { title: 'Frontpage|layout/addons/AddonsLayout', component: AddonsLayout, excludeStories: ['data'], + decorators: [UseAddonsSearchDecorator], }; export const Base = () => children; @@ -20,48 +21,29 @@ export const HideTableOfContents = () => ( ); export const SearchLoading = () => ( - - children - + children ); -const relatedTags = [ - { - link: '/notes', - name: 'πŸ—’ Notes', - }, - { - link: '/storybook', - name: 'πŸ“• Storybook', - }, - { - link: '/qa', - name: 'πŸ•΅οΈβ€β™€οΈ QA', - }, - { - link: '/prototype', - name: '✨ Prototype', - }, - { - link: '/testing', - name: 'βœ… Testing', - }, - { - link: '/deploy', - name: '☁️ Deploy', - }, -]; +SearchLoading.parameters = { + isSearching: true, + isSearchLoading: true, +}; export const SearchResults = () => ( - - children - + children ); + +SearchResults.parameters = { + isSearching: true, + isSearchLoading: false, +}; + +export const SearchNoResults = () => ( + children +); + +SearchNoResults.parameters = { + isSearching: true, + isSearchLoading: false, + noResults: true, +}; diff --git a/src/components/layout/addons/AddonsList.js b/src/components/layout/addons/AddonsList.js index 5311cef8..06b0689c 100644 --- a/src/components/layout/addons/AddonsList.js +++ b/src/components/layout/addons/AddonsList.js @@ -12,7 +12,16 @@ const ListWrapper = styled.div` } `; -export const AddonsList = ({ addonItems, isLoading, ...props }) => { +const loadingItems = [ + { id: '1', isLoading: true }, + { id: '2', isLoading: true }, + { id: '3', isLoading: true }, + { id: '4', isLoading: true }, + { id: '5', isLoading: true }, + { id: '6', isLoading: true }, +]; + +export const AddonsList = ({ addonItems, isLoading, from, ...props }) => { const [visibleCount, setVisibleCount] = useState(6); const items = useMemo(() => addonItems.slice(0, visibleCount), [visibleCount, addonItems]); @@ -27,8 +36,8 @@ export const AddonsList = ({ addonItems, isLoading, ...props }) => { aria-busy={!!isLoading} {...props} > - {items.map((addon) => ( - + {(isLoading ? loadingItems : items).map((addon) => ( + ))} {addonItems.length > 6 && visibleCount < addonItems.length && (