├── .circleci └── config.yml ├── .eslintrc.js ├── .flowconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .storybook ├── .babelrc ├── components │ ├── Showcase │ │ ├── Showcase.js │ │ └── index.js │ └── Wrapper │ │ ├── Wrapper.js │ │ └── index.js ├── config.js ├── manager-head.html └── webpack-config.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── PRIVACY.md ├── README.md ├── codecov.yml ├── config ├── .eslintrc.js ├── env.js ├── enzyme-setup.js ├── jest │ ├── babelTransform.js │ ├── cssTransform.js │ ├── electron.js │ ├── fileTransform.js │ └── graphqlTransform.js ├── paths.js ├── polyfills.js ├── testFrameworkSetup.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── docs ├── adding-a-project.md ├── dev-info │ ├── code-structure.md │ ├── misc │ │ └── icon-creation.md │ └── style-guide.md ├── getting-started.md ├── images │ ├── add-dep-button.png │ ├── dev-server-module.png │ ├── dev-server-toggle.png │ ├── main-image.png │ ├── search-deps.png │ ├── task-details-toggle.png │ └── task-row-toggle.png └── understanding-package.json.md ├── logo.png ├── package.json ├── prettier.config.js ├── public ├── 256x256.png ├── guppy-logo.svg ├── icon.png └── index.html ├── scripts ├── .eslintrc.js ├── build.js ├── start.js └── test.js ├── src ├── __mocks__ │ ├── electron-log.js │ ├── electron-store.js │ ├── electron-updater.js │ ├── electron.js │ └── mixpanel-browser.js ├── actions │ ├── index.js │ └── types.js ├── assets │ ├── fonts │ │ ├── fira-mono │ │ │ ├── FiraMono-Bold.ttf │ │ │ ├── FiraMono-Medium.ttf │ │ │ └── FiraMono-Regular.ttf │ │ └── futura │ │ │ ├── FuturaPT-Bold.woff │ │ │ ├── FuturaPT-Book.woff │ │ │ ├── FuturaPT-Demi.woff │ │ │ ├── FuturaPT-Light.woff │ │ │ └── FuturaPT-Medium.woff │ ├── icons │ │ ├── mac │ │ │ └── logo.icns │ │ ├── png │ │ │ ├── 1024x1024.png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ ├── 64x64.png │ │ │ └── 96x96.png │ │ └── win │ │ │ └── logo.ico │ └── images │ │ ├── gatsby.png │ │ ├── gatsby_small.png │ │ ├── guppy-loader.gif │ │ ├── guppy-logo.svg │ │ ├── icons │ │ ├── icon_blueorange.jpg │ │ ├── icon_cactiteapot.jpg │ │ ├── icon_fish1.jpg │ │ ├── icon_fish2.jpg │ │ ├── icon_fish3.jpg │ │ ├── icon_fish4.jpg │ │ ├── icon_fish5.jpg │ │ ├── icon_fish6.jpg │ │ ├── icon_fish7.jpg │ │ ├── icon_fist.jpg │ │ ├── icon_gradient1.png │ │ ├── icon_gradient2.png │ │ ├── icon_gradient3.png │ │ ├── icon_gradient4.png │ │ ├── icon_pineapple.jpg │ │ ├── icon_pinkbird.jpg │ │ ├── icon_roundbird.jpg │ │ ├── icon_squirrel.jpg │ │ ├── icon_teeth.jpg │ │ ├── icon_wateryorange.jpg │ │ └── icon_yellowcup.jpg │ │ ├── nextjs.png │ │ ├── nextjs_small.png │ │ └── react-icon.svg ├── base.css ├── components │ ├── App │ │ ├── App.js │ │ └── index.js │ ├── AppSettingsModal │ │ ├── AppSettingsModal.js │ │ ├── AppSettingsModal.test.js │ │ ├── __snapshots__ │ │ │ └── AppSettingsModal.test.js.snap │ │ └── index.js │ ├── ApplicationMenu │ │ ├── ApplicationMenu.js │ │ └── index.js │ ├── AvailableWidth │ │ ├── AvailableWidth.js │ │ └── index.js │ ├── BigClickableButton │ │ ├── BigClickableButton.js │ │ ├── BigClickableButton.test.js │ │ ├── __snapshots__ │ │ │ └── BigClickableButton.test.js.snap │ │ └── index.js │ ├── Button │ │ ├── ButtonBase.js │ │ ├── ButtonBase.stories.js │ │ ├── ButtonBase.test.js │ │ ├── FillButton.js │ │ ├── FillButton.stories.js │ │ ├── FillButton.test.js │ │ ├── StrokeButton.js │ │ ├── StrokeButton.stories.js │ │ ├── StrokeButton.test.js │ │ ├── __snapshots__ │ │ │ ├── ButtonBase.test.js.snap │ │ │ ├── FillButton.test.js.snap │ │ │ └── StrokeButton.test.js.snap │ │ └── index.js │ ├── ButtonWithIcon │ │ ├── ButtonWithIcon.js │ │ ├── ButtonWithIcon.test.js │ │ ├── ButtonsWithIcon.stories.js │ │ ├── __snapshots__ │ │ │ └── ButtonWithIcon.test.js.snap │ │ └── index.js │ ├── Card │ │ ├── Card.js │ │ └── index.js │ ├── CircularOutline │ │ ├── CircularOutline.js │ │ ├── CircularOutline.stories.js │ │ └── index.js │ ├── CodesandboxLogo │ │ ├── Logo.js │ │ └── index.js │ ├── CreateNewProjectWizard │ │ ├── BuildPane.js │ │ ├── BuildStepProgress.js │ │ ├── CreateNewProjectWizard.js │ │ ├── Gatsby │ │ │ ├── MockStarterYaml.js │ │ │ ├── ProjectStarterSelection.js │ │ │ ├── ProjectStarterSelection.test.js │ │ │ ├── SelectStarterList.js │ │ │ └── SelectStarterList.test.js │ │ ├── ImportExisting.js │ │ ├── MainPane.js │ │ ├── ProjectName.js │ │ ├── ProjectPath.js │ │ ├── SubmitButton.js │ │ ├── SummaryPane.js │ │ ├── __tests__ │ │ │ ├── BuildPane.test.js │ │ │ ├── BuildStepProgress.test.js │ │ │ ├── CreateNewProjectWizard.test.js │ │ │ ├── ImportExisting.test.js │ │ │ ├── MainPane.test.js │ │ │ ├── ProjectName.test.js │ │ │ ├── ProjectPath.test.js │ │ │ ├── SubmitButton.test.js │ │ │ ├── SummaryPane.test.js │ │ │ ├── __snapshots__ │ │ │ │ ├── BuildPane.test.js.snap │ │ │ │ ├── BuildStepProgress.test.js.snap │ │ │ │ ├── CreateNewProjectWizard.test.js.snap │ │ │ │ ├── ImportExisting.test.js.snap │ │ │ │ ├── MainPane.test.js.snap │ │ │ │ ├── ProjectPath.test.js.snap │ │ │ │ └── SummaryPane.test.js.snap │ │ │ └── helpers.test.js │ │ ├── helpers.js │ │ ├── index.js │ │ └── types.js │ ├── CustomHighlight │ │ ├── CustomHighlight.js │ │ └── index.js │ ├── Debounced │ │ ├── Debounced.js │ │ └── index.js │ ├── DependencyManagementPane │ │ ├── AddDependencyInitialScreen.js │ │ ├── AddDependencyModal.js │ │ ├── AddDependencySearchBox.js │ │ ├── AddDependencySearchProvider.js │ │ ├── AddDependencySearchResult.js │ │ ├── AlgoliaLogo.js │ │ ├── DeleteDependencyButton.js │ │ ├── DependencyDetails.js │ │ ├── DependencyDetailsTable.js │ │ ├── DependencyInfoFromNpm.js │ │ ├── DependencyInstalling.js │ │ ├── DependencyManagementPane.js │ │ ├── DependencyUpdateRow.js │ │ ├── __mocks__ │ │ │ └── dependency.js │ │ ├── __tests__ │ │ │ ├── AddDependencyInitialScreen.test.js │ │ │ ├── AddDependencyModal.test.js │ │ │ ├── AddDependencySearchBox.test.js │ │ │ ├── AddDependencySearchProvider.test.js │ │ │ ├── AddDependencySearchResult.test.js │ │ │ ├── DependencyDetails.test.js │ │ │ ├── DependencyInstalling.test.js │ │ │ ├── DependencyManagementPane.test.js │ │ │ └── __snapshots__ │ │ │ │ ├── AddDependencyInitialScreen.test.js.snap │ │ │ │ ├── AddDependencySearchBox.test.js.snap │ │ │ │ ├── AddDependencySearchProvider.test.js.snap │ │ │ │ ├── AddDependencySearchResult.test.js.snap │ │ │ │ ├── DependencyDetails.test.js.snap │ │ │ │ ├── DependencyInstalling.test.js.snap │ │ │ │ └── DependencyManagementPane.test.js.snap │ │ └── index.js │ ├── DetectActive │ │ ├── DetectActive.js │ │ └── index.js │ ├── DevTools │ │ ├── index.dev.js │ │ ├── index.js │ │ └── index.prod.js │ ├── DevelopmentServerPane │ │ ├── DevelopmentServerPane.js │ │ ├── DevelopmentServerPane.test.js │ │ ├── __snapshots__ │ │ │ └── DevelopmentServerPane.test.js.snap │ │ └── index.js │ ├── DevelopmentServerStatus │ │ ├── DevelopmentServerStatus.js │ │ └── index.js │ ├── DirectoryPicker │ │ ├── DirectoryPicker.js │ │ └── index.js │ ├── Divider │ │ ├── Divider.js │ │ └── index.js │ ├── Earth │ │ ├── Earth.js │ │ └── index.js │ ├── EjectButton │ │ ├── EjectButton.js │ │ ├── EjectButton.test.js │ │ ├── __snapshots__ │ │ │ └── EjectButton.test.js.snap │ │ └── index.js │ ├── ExternalLink │ │ ├── ExternalLink.js │ │ └── index.js │ ├── FadeIn │ │ ├── FadeIn.js │ │ └── index.js │ ├── FadeOnChange │ │ ├── FadeOnChange.js │ │ └── index.js │ ├── FeedbackButton │ │ ├── FeedbackButton.js │ │ └── index.js │ ├── FormField │ │ ├── FormField.js │ │ └── index.js │ ├── FullWidth │ │ ├── FullWidth.js │ │ └── index.js │ ├── Heading │ │ ├── Heading.js │ │ └── index.js │ ├── HelpButton │ │ ├── HelpButton.js │ │ └── index.js │ ├── HoverableOutlineButton │ │ ├── HoverableOutlineButton.js │ │ └── index.js │ ├── Icons │ │ └── index.js │ ├── ImportProjectButton │ │ ├── ImportProjectButton.js │ │ └── index.js │ ├── Initialization │ │ ├── Initialization.helpers.test.js │ │ ├── Initialization.js │ │ └── index.js │ ├── IntroScreen │ │ ├── IntroScreen.js │ │ └── index.js │ ├── Label │ │ ├── Label.js │ │ └── index.js │ ├── LargeLED │ │ ├── LargeLED.helpers.js │ │ ├── LargeLED.js │ │ └── index.js │ ├── License │ │ ├── License.js │ │ └── index.js │ ├── LoadingScreen │ │ ├── LoadingScreen.js │ │ └── index.js │ ├── Logo │ │ ├── Logo.js │ │ ├── Logo.stories.js │ │ └── index.js │ ├── MainContentWrapper │ │ ├── MainContentWrapper.js │ │ └── index.js │ ├── Middot │ │ ├── Middot.js │ │ └── index.js │ ├── Modal │ │ ├── Modal.js │ │ └── index.js │ ├── ModalHeader │ │ ├── ModalHeader.js │ │ └── index.js │ ├── Module │ │ ├── Module.js │ │ └── index.js │ ├── MountAfter │ │ ├── MountAfter.js │ │ ├── MountAfter.stories.js │ │ └── index.js │ ├── NodeProvider │ │ ├── NodeProvider.js │ │ └── index.js │ ├── OnlineChecker │ │ ├── OnlineChecker.js │ │ └── index.js │ ├── OnlyOn │ │ ├── OnlyOn.js │ │ └── index.js │ ├── Paragraph │ │ ├── Paragraph.js │ │ └── index.js │ ├── PixelShifter │ │ ├── PixelShifter.js │ │ └── index.js │ ├── Planet │ │ ├── EarthContinents.js │ │ ├── Orbit.js │ │ ├── Planet.helpers.js │ │ ├── Planet.js │ │ ├── Planet.stories.js │ │ ├── PlanetCloud.js │ │ ├── PlanetGlow.js │ │ ├── PlanetMoon.js │ │ └── index.js │ ├── ProgressBar │ │ ├── ProgressBar.js │ │ ├── ProgressBar.stories.js │ │ ├── ProgressBar.test.js │ │ ├── __snapshots__ │ │ │ └── ProgressBar.test.js.snap │ │ └── index.js │ ├── ProjectConfigurationModal │ │ ├── ProjectConfigurationModal.js │ │ ├── ProjectConfigurationModal.test.js │ │ ├── __snapshots__ │ │ │ └── ProjectConfigurationModal.test.js.snap │ │ └── index.js │ ├── ProjectIconSelection │ │ ├── ProjectIconSelection.js │ │ ├── ProjectIconSelection.stories.js │ │ └── index.js │ ├── ProjectPage │ │ ├── ProjectPage.js │ │ ├── ProjectPage.test.js │ │ ├── __snapshots__ │ │ │ └── ProjectPage.test.js.snap │ │ └── index.js │ ├── ProjectTitle │ │ ├── ProjectTitle.js │ │ ├── ProjectTitle.stories.js │ │ └── index.js │ ├── ProjectTypeSelection │ │ ├── ProjectTypeSelection.js │ │ └── index.js │ ├── ScrollDisabler │ │ ├── ScrollDisabler.js │ │ └── index.js │ ├── SelectableImage │ │ ├── SelectableImage.js │ │ └── index.js │ ├── SelectableItem │ │ ├── SelectableItem.js │ │ └── index.js │ ├── SettingsButton │ │ ├── SettingsButton.js │ │ └── index.js │ ├── Sidebar │ │ ├── AddProjectButton.js │ │ ├── IntroductionBlurb.js │ │ ├── Sidebar.js │ │ ├── Sidebar.test.js │ │ ├── SidebarProjectIcon.js │ │ ├── SidebarProjectIcon.test.js │ │ └── index.js │ ├── Spacer │ │ ├── Spacer.js │ │ └── index.js │ ├── Spinner │ │ ├── Spinner.js │ │ └── index.js │ ├── TaskDetailsModal │ │ ├── TaskDetailsModal.js │ │ └── index.js │ ├── TaskRunnerPane │ │ ├── TaskRunnerPane.js │ │ ├── TaskRunnerPane.test.js │ │ └── index.js │ ├── TaskRunnerPaneRow │ │ ├── TaskRunnerPaneRow.js │ │ └── index.js │ ├── TerminalOutput │ │ ├── TerminalOutput.js │ │ ├── TerminalOutput.test.js │ │ ├── index.js │ │ └── localLinksAddon.js │ ├── TextButton │ │ ├── TextButton.js │ │ └── index.js │ ├── TextInput │ │ ├── TextInput.js │ │ └── index.js │ ├── TextInputWithButton │ │ ├── TextInputWithButton.js │ │ └── index.js │ ├── Titlebar │ │ ├── Titlebar.js │ │ └── index.js │ ├── Toggle │ │ ├── Toggle.js │ │ ├── Toggle.stories.js │ │ └── index.js │ ├── TwoPaneModal │ │ ├── TwoPaneModal.js │ │ └── index.js │ ├── WhimsicalInstaller │ │ ├── File.js │ │ ├── Folder.js │ │ ├── WhimsicalInstaller.helpers.js │ │ ├── WhimsicalInstaller.helpers.test.js │ │ ├── WhimsicalInstaller.js │ │ ├── WhimsicalInstaller.stories.js │ │ ├── WhimsicalInstaller.types.js │ │ └── index.js │ └── WindowDimensions │ │ ├── WindowDimensions.js │ │ └── index.js ├── config │ ├── app.js │ └── project-types.js ├── constants.js ├── electron.js ├── electron.test.js ├── fonts.css ├── global-styles.js ├── index.js ├── reducers │ ├── __snapshots__ │ │ └── projects.reducer.test.js.snap │ ├── app-loaded.reducer.js │ ├── app-settings.reducer.js │ ├── app-settings.reducer.test.js │ ├── app-status.reducer.js │ ├── app-status.reducer.test.js │ ├── dependencies.reducer.js │ ├── dependencies.reducer.test.js │ ├── index.js │ ├── modal.reducer.js │ ├── modal.reducer.test.js │ ├── onboarding-status.reducer.js │ ├── paths.reducer.js │ ├── paths.reducer.test.js │ ├── projects.reducer.js │ ├── projects.reducer.test.js │ ├── queue.reducer.js │ ├── queue.reducer.test.js │ ├── tasks.reducer.js │ └── tasks.reducer.test.js ├── sagas │ ├── analytics.saga.js │ ├── analytics.saga.test.js │ ├── delete-project.saga.js │ ├── delete-project.saga.test.js │ ├── dependency.saga.js │ ├── dependency.saga.test.js │ ├── development.saga.js │ ├── development.saga.test.js │ ├── import-project.saga.js │ ├── import-project.saga.test.js │ ├── index.js │ ├── queue.saga.js │ ├── queue.saga.test.js │ ├── refresh-projects.saga.js │ ├── refresh-projects.saga.test.js │ ├── save-project-settings.saga.js │ ├── save-project-settings.saga.test.js │ ├── task.saga.js │ └── task.saga.test.js ├── services │ ├── analytics.service.js │ ├── analytics.service.test.js │ ├── check-if-url-exists.service.js │ ├── config-variables.service.js │ ├── config-variables.service.test.js │ ├── create-project.fixtures.js │ ├── create-project.service.js │ ├── create-project.service.test.js │ ├── dependencies.service.js │ ├── electron-store.service.js │ ├── find-available-port.service.js │ ├── kill-process-id.service.js │ ├── kill-process-id.service.test.js │ ├── platform.service.js │ ├── platform.service.test.js │ ├── process-logger.service.js │ ├── project-name-service.test.js │ ├── project-name.service.js │ ├── project-type-specifics.js │ ├── project-type-specifics.test.js │ ├── read-from-disk.service.js │ └── shell.service.js ├── store │ ├── index.js │ ├── migrations.js │ ├── migrations.test.js │ └── storage-engine.js ├── stories │ └── colors.stories.js ├── test-helpers │ └── factories.js ├── types.js └── utils.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: node:8.10 7 | 8 | working_directory: ~/repo 9 | 10 | steps: 11 | - checkout 12 | 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # don't fallback to using the latest cache if no exact match is found 18 | # because it yarn doesn't pick some changes (for example, flow doesn't update) 19 | 20 | - run: yarn install 21 | 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: v1-dependencies-{{ checksum "package.json" }} 26 | 27 | # run tests! 28 | - run: yarn test:report-coverage 29 | 30 | # Flowjs check 31 | - run: yarn flow 32 | 33 | # Linter 34 | - run: yarn lint 35 | 36 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['react-app', 'plugin:jest/recommended'], 3 | parserOptions: { 4 | ecmaVersion: 2015, 5 | }, 6 | rules: { 7 | 'no-unused-vars': 1, 8 | 'no-shadow': 2, 9 | 'flowtype/require-valid-file-annotation': [ 10 | 2, 11 | 'always', 12 | { 13 | annotationStyle: 'line', 14 | }, 15 | ], 16 | 'flowtype/space-after-type-colon': 0, 17 | 'flowtype/generic-spacing': 0, 18 | 'jest/no-large-snapshots': ['warn', { maxSize: 100 }], 19 | }, 20 | overrides: [ 21 | { 22 | files: ['*.test.js', '*.spec.js'], 23 | rules: { 24 | 'flowtype/require-valid-file-annotation': 0, 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | # `electron-packager` comes with intentionally-bad JSON, and Flow trips over it 3 | .*/node_modules/electron-packager/test/fixtures/infer-malformed-json/package.json 4 | 5 | # Graphql contains a typing error --> ignore it 6 | .*/node_modules/graphql/**/**/*.js 7 | 8 | # Release builds are git-ignored, and thus can cause problems when checking out 9 | # new branches, and have Flow run on old files without the same dependencies. 10 | .*/release-builds/* 11 | 12 | # Ignore styled-components as there are many flow errors 13 | # Maybe related to https://github.com/styled-components/styled-components/issues/302 14 | .*/node_modules/styled-components/**/*.js 15 | 16 | [include] 17 | 18 | [libs] 19 | 20 | [lints] 21 | 22 | [options] 23 | 24 | [strict] 25 | 26 | [untyped] 27 | # React-beautiful-dnd contains a typing error --> ignore it 28 | .*/node_modules/react-beautiful-dnd/.* 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know if something's broken 4 | 5 | --- 6 | 7 | 10 | 11 | 12 | **Describe the bug** 13 | 14 | 15 | **To Reproduce** 16 | 23 | 24 | **Expected behavior** 25 | 26 | 27 | **Screenshots** 28 | 29 | 30 | **Environment (please complete the following information):** 31 | - OS: 32 | - Version: 33 | - Node version: 34 | 35 | **Additional context** 36 | 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 12 | 13 | 14 | **Is your feature request related to a problem? Please describe.** 15 | 16 | 17 | **Describe the solution you'd like** 18 | 19 | 20 | **Describe alternatives you've considered** 21 | 22 | 23 | **Additional context** 24 | 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | **Related Issue:** 9 | 12 | 13 | **Summary:** 14 | 17 | 18 | **Screenshots/GIFs:** 19 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /release-builds 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # because we use yarn.lock 26 | package-lock.json 27 | 28 | # storybook 29 | /storybook-static 30 | 31 | # coverage 32 | coverage* -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ], 5 | "plugins": [ 6 | "react-docgen" 7 | ] 8 | } -------------------------------------------------------------------------------- /.storybook/components/Showcase/Showcase.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * NOTE: This component is meant to be used within React Storybook. 4 | * It's a dev-only component, not meant to be used within Guppy. 5 | */ 6 | import React, { Fragment, Component } from 'react'; 7 | import styled from 'styled-components'; 8 | 9 | import { COLORS } from '../../../src/constants'; 10 | 11 | import Heading from '../../../src/components/Heading'; 12 | 13 | type Props = { 14 | label: string, 15 | children: React$Node, 16 | }; 17 | 18 | class Showcase extends Component { 19 | render() { 20 | const { label, children } = this.props; 21 | 22 | return ( 23 | 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | } 30 | 31 | const Wrapper = styled.div` 32 | display: flex; 33 | border-bottom: 1px solid rgba(0, 0, 0, 0.25); 34 | 35 | &:last-of-type { 36 | border-bottom: none; 37 | } 38 | `; 39 | 40 | const Label = styled(Heading)` 41 | display: flex; 42 | width: 150px; 43 | padding: 1rem 0; 44 | border-right: 1px solid rgba(0, 0, 0, 0.08); 45 | `; 46 | 47 | const MainContent = styled.div` 48 | padding: 1.5rem; 49 | flex: 1; 50 | `; 51 | 52 | export default Showcase; 53 | -------------------------------------------------------------------------------- /.storybook/components/Showcase/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Showcase'; 2 | -------------------------------------------------------------------------------- /.storybook/components/Wrapper/Wrapper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { COLORS } from '../../../src/constants'; 6 | 7 | type Props = { 8 | children: any, 9 | }; 10 | 11 | class Wrapper extends Component { 12 | render() { 13 | const { children } = this.props; 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | } 20 | } 21 | 22 | const OuterWrapper = styled.div` 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | `; 29 | const InnerWrapper = styled.div` 30 | background: ${COLORS.lightBackground}; 31 | box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1); 32 | margin: 2rem; 33 | padding: 1.5rem; 34 | max-width: 1100px; 35 | border-radius: 4px; 36 | `; 37 | 38 | export default Wrapper; 39 | -------------------------------------------------------------------------------- /.storybook/components/Wrapper/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Wrapper'; 2 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies, import/no-unresolved, import/extensions */ 2 | import React from 'react'; 3 | import { configure, addDecorator } from '@storybook/react'; 4 | 5 | import { COLORS } from '../src/constants'; 6 | import Wrapper from './components/Wrapper'; 7 | 8 | import '../src/global-styles'; 9 | 10 | const WrapperDecorator = storyFn => {storyFn()}; 11 | 12 | addDecorator(WrapperDecorator); 13 | 14 | function loadStories() { 15 | const stories = require.context('../src', true, /.stories.js$/); 16 | 17 | stories.keys().forEach(filename => stories(filename)); 18 | } 19 | 20 | configure(loadStories, module); 21 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.storybook/webpack-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpackConfig = require('../config/webpack.config.dev'); 3 | 4 | module.exports = (baseConfig, configType, defaultConfig) => { 5 | // Storybook uses its own Webpack config. For consistency, we want to use 6 | // the same JS config as we use in Guppy's Webpack config. 7 | const guppyJsConfig = webpackConfig.module.rules[1]; 8 | 9 | defaultConfig.module.rules.shift(); 10 | defaultConfig.module.rules.unshift(guppyJsConfig); 11 | 12 | return defaultConfig; 13 | }; 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "flowtype.flow-for-vscode", 6 | "jpoissonnier.vscode-styled-components" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Jest Tests", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 12 | "cwd": "${workspaceRoot}", 13 | "args": [ 14 | "--i", 15 | ], 16 | "console": "integratedTerminal", 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/release-builds": true 6 | }, 7 | "typescript.validate.enable": false, 8 | "javascript.validate.enable": false, 9 | "flow.enabled": true, 10 | "eslint.enable": true, 11 | "prettier.singleQuote": true, 12 | "prettier.trailingComma": "es5", 13 | "prettier.parser": "flow", 14 | "editor.formatOnSave": true 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2018-present, Joshua Comeau 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | comment: 6 | layout: "reach, diff, flags, files" 7 | behavior: default 8 | require_changes: true # if true: only post the comment if coverage changes 9 | require_base: no # [yes :: must have a base report to post] 10 | require_head: yes # [yes :: must have a head report to post] 11 | branches: null 12 | 13 | coverage: 14 | precision: 2 15 | round: down 16 | range: "70...100" 17 | -------------------------------------------------------------------------------- /config/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // no flow in this folder 4 | 'flowtype/require-valid-file-annotation': 0, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /config/enzyme-setup.js: -------------------------------------------------------------------------------- 1 | const Enzyme = require('enzyme'); 2 | const Adapter = require('enzyme-adapter-react-16'); 3 | 4 | // React 16 Enzyme adapter 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const babelJest = require('babel-jest'); 3 | 4 | module.exports = babelJest.createTransformer({ 5 | presets: [require.resolve('babel-preset-react-app')], 6 | }); 7 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | //@flow 2 | // This is a custom Jest transformer turning style imports into empty objects. 3 | // http://facebook.github.io/jest/docs/en/webpack.html 4 | 5 | module.exports = { 6 | process() { 7 | return 'module.exports = {};'; 8 | }, 9 | getCacheKey() { 10 | // The output is always the same. 11 | return 'cssTransform'; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /config/jest/electron.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | global.window = { 3 | require: require, 4 | }; 5 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | const path = require('path'); 3 | 4 | // This is a custom Jest transformer turning file imports into filenames. 5 | // http://facebook.github.io/jest/docs/en/webpack.html 6 | 7 | module.exports = { 8 | process(src, filename) { 9 | const assetFilename = JSON.stringify(path.basename(filename)); 10 | 11 | if (filename.match(/\.svg$/)) { 12 | return `module.exports = { 13 | __esModule: true, 14 | default: ${assetFilename}, 15 | ReactComponent: () => ${assetFilename}, 16 | };`; 17 | } 18 | 19 | return `module.exports = ${assetFilename};`; 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /config/jest/graphqlTransform.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const loader = require('graphql-tag/loader'); 3 | 4 | module.exports = { 5 | process(src: any) { 6 | return loader.call({ cacheable() {} }, src); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 2 | // We don't polyfill it in the browser--this is user's responsibility. 3 | if (process.env.NODE_ENV === 'test') { 4 | require('raf').polyfill(global); 5 | } 6 | -------------------------------------------------------------------------------- /config/testFrameworkSetup.js: -------------------------------------------------------------------------------- 1 | const { JSDOM } = require('jsdom'); 2 | const jsdom = new JSDOM(''); 3 | const { window } = jsdom; 4 | const fetch = require('jest-fetch-mock'); 5 | 6 | global.document = window.document; 7 | global.window = window; 8 | 9 | global.navigator = { 10 | userAgent: 'node.js', 11 | }; 12 | global.requestAnimationFrame = function(callback) { 13 | return setTimeout(callback, 0); 14 | }; 15 | global.cancelAnimationFrame = function(id) { 16 | clearTimeout(id); 17 | }; 18 | 19 | // Import BrowserAPI to Node env 20 | require('jsdom-global')(); 21 | // Import test framework for styled components for better snapshot messages 22 | require('jest-styled-components'); 23 | 24 | jest.setMock('node-fetch', fetch); 25 | -------------------------------------------------------------------------------- /docs/dev-info/misc/icon-creation.md: -------------------------------------------------------------------------------- 1 | # Icon Creation 2 | 3 | Application icons for Guppy projects are finicky; each platform has its own format. 4 | 5 | Happily, [Electron Icon Maker](https://github.com/jaretburkett/electron-icon-maker) solves a lot of it for us! 6 | 7 | Here's the steps that were taken the last time our icon needed updating: 8 | 9 | ``` 10 | npm i -g electron-icon-maker 11 | electron-icon-maker -i /path/to/logo.png -o ./src/assets/icons 12 | ``` 13 | 14 | Additionally, a few small tweaks are necessary: 15 | 16 | - by default, the tool names Windows/Mac icon files icon.ico and icon.icns, respectively. We use logo.ico and logo.icns, so make sure to rename to match this convention so that bundling works correctly. 17 | - for whatever reason, the tool does not create png/96x96.png, so you'll still have to generate that manually 18 | 19 | If we find ourselves needing to create icons a lot, for some reason, we could remove these edge-cases by using the default name of `icon.x`, as well as not including a 96x96 icon. 20 | -------------------------------------------------------------------------------- /docs/images/add-dep-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/add-dep-button.png -------------------------------------------------------------------------------- /docs/images/dev-server-module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/dev-server-module.png -------------------------------------------------------------------------------- /docs/images/dev-server-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/dev-server-toggle.png -------------------------------------------------------------------------------- /docs/images/main-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/main-image.png -------------------------------------------------------------------------------- /docs/images/search-deps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/search-deps.png -------------------------------------------------------------------------------- /docs/images/task-details-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/task-details-toggle.png -------------------------------------------------------------------------------- /docs/images/task-row-toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/docs/images/task-row-toggle.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/logo.png -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | parser: 'flow' 5 | }; 6 | -------------------------------------------------------------------------------- /public/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/public/256x256.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /scripts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // we have node scripts so we want to use strict mode 4 | strict: 0, 5 | // no flow in this folder 6 | 'flowtype/require-valid-file-annotation': 0, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'test'; 3 | process.env.NODE_ENV = 'test'; 4 | process.env.PUBLIC_URL = ''; 5 | 6 | // Makes the script crash on unhandled rejections instead of silently 7 | // ignoring them. In the future, promise rejections that are not handled will 8 | // terminate the Node.js process with a non-zero exit code. 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | // Ensure environment variables are read. 14 | require('../config/env'); 15 | 16 | // eslint-disable-next-line jest/no-jest-import 17 | const jest = require('jest'); 18 | let argv = process.argv.slice(2); 19 | 20 | // Watch unless on CI, in coverage mode, or explicitly running all tests 21 | if ( 22 | !process.env.CI && 23 | argv.indexOf('--coverage') === -1 && 24 | argv.indexOf('--watchAll') === -1 25 | ) { 26 | argv.push('--watch'); 27 | } 28 | 29 | jest.run(argv); 30 | -------------------------------------------------------------------------------- /src/__mocks__/electron-log.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | 3 | module.exports = { 4 | transports: { 5 | file: { 6 | level: 'info', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/__mocks__/electron-store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | module.exports = function() { 3 | let store = {}; 4 | 5 | this.clear = jest.fn(() => { 6 | store = {}; 7 | }); 8 | 9 | this.get = jest.fn(key => store[key]); 10 | this.set = jest.fn((key, val) => { 11 | store[key] = val; 12 | return val; 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/__mocks__/electron-updater.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | 3 | module.exports = { 4 | autoUpdater: { 5 | checkForUpdatesAndNotify: jest.fn(), 6 | on: jest.fn(), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/__mocks__/electron.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import * as path from 'path'; 3 | 4 | module.exports = { 5 | ipcRenderer: { 6 | send: jest.fn(), 7 | }, 8 | remote: { 9 | app: { 10 | getAppPath: () => path.resolve(__dirname, '..', '..', '..'), 11 | getPath: () => 12 | process.env.APPDATA || 13 | (process.platform === 'darwin' 14 | ? process.env.HOME + 'Library/Preferences' 15 | : '/var/local'), 16 | }, 17 | dialog: { 18 | showErrorBox: jest.fn(), 19 | showOpenDialog: jest.fn(), 20 | showMessageBox: jest.fn(), 21 | }, 22 | shell: { 23 | moveItemToTrash: jest.fn(), 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/__mocks__/mixpanel-browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | module.exports = { 3 | init: jest.fn(), 4 | identify: jest.fn(), 5 | track: jest.fn(), 6 | has_opted_out_tracking: jest.fn(() => false), 7 | }; 8 | -------------------------------------------------------------------------------- /src/assets/fonts/fira-mono/FiraMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/fira-mono/FiraMono-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fira-mono/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/fira-mono/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fira-mono/FiraMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/fira-mono/FiraMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/futura/FuturaPT-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/futura/FuturaPT-Bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/futura/FuturaPT-Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/futura/FuturaPT-Book.woff -------------------------------------------------------------------------------- /src/assets/fonts/futura/FuturaPT-Demi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/futura/FuturaPT-Demi.woff -------------------------------------------------------------------------------- /src/assets/fonts/futura/FuturaPT-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/futura/FuturaPT-Light.woff -------------------------------------------------------------------------------- /src/assets/fonts/futura/FuturaPT-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/fonts/futura/FuturaPT-Medium.woff -------------------------------------------------------------------------------- /src/assets/icons/mac/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/mac/logo.icns -------------------------------------------------------------------------------- /src/assets/icons/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/1024x1024.png -------------------------------------------------------------------------------- /src/assets/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/128x128.png -------------------------------------------------------------------------------- /src/assets/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/16x16.png -------------------------------------------------------------------------------- /src/assets/icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/24x24.png -------------------------------------------------------------------------------- /src/assets/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/256x256.png -------------------------------------------------------------------------------- /src/assets/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/32x32.png -------------------------------------------------------------------------------- /src/assets/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/48x48.png -------------------------------------------------------------------------------- /src/assets/icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/512x512.png -------------------------------------------------------------------------------- /src/assets/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/64x64.png -------------------------------------------------------------------------------- /src/assets/icons/png/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/png/96x96.png -------------------------------------------------------------------------------- /src/assets/icons/win/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/icons/win/logo.ico -------------------------------------------------------------------------------- /src/assets/images/gatsby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/gatsby.png -------------------------------------------------------------------------------- /src/assets/images/gatsby_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/gatsby_small.png -------------------------------------------------------------------------------- /src/assets/images/guppy-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/guppy-loader.gif -------------------------------------------------------------------------------- /src/assets/images/icons/icon_blueorange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_blueorange.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_cactiteapot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_cactiteapot.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish1.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish2.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish3.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish4.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish5.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish6.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fish7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fish7.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_fist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_fist.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_gradient1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_gradient1.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon_gradient2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_gradient2.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon_gradient3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_gradient3.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon_gradient4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_gradient4.png -------------------------------------------------------------------------------- /src/assets/images/icons/icon_pineapple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_pineapple.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_pinkbird.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_pinkbird.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_roundbird.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_roundbird.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_squirrel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_squirrel.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_teeth.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_teeth.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_wateryorange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_wateryorange.jpg -------------------------------------------------------------------------------- /src/assets/images/icons/icon_yellowcup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/icons/icon_yellowcup.jpg -------------------------------------------------------------------------------- /src/assets/images/nextjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/nextjs.png -------------------------------------------------------------------------------- /src/assets/images/nextjs_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshwcomeau/guppy/72d84495d9c15d22053b0010ac1fd9a1061a7054/src/assets/images/nextjs_small.png -------------------------------------------------------------------------------- /src/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:before, 3 | *:after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body, 9 | input, 10 | button, 11 | select, 12 | option { 13 | font-family: 'Futura PT'; 14 | } 15 | 16 | /* http://meyerweb.com/eric/tools/css/reset/ 17 | v2.0 | 20110126 18 | License: none (public domain) 19 | */ 20 | html, 21 | body, 22 | div, 23 | span, 24 | applet, 25 | object, 26 | iframe, 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6, 33 | p, 34 | blockquote, 35 | pre, 36 | a, 37 | abbr, 38 | acronym, 39 | address, 40 | big, 41 | cite, 42 | code, 43 | del, 44 | dfn, 45 | em, 46 | img, 47 | ins, 48 | kbd, 49 | q, 50 | s, 51 | samp, 52 | small, 53 | strike, 54 | strong, 55 | sub, 56 | sup, 57 | tt, 58 | var, 59 | b, 60 | u, 61 | i, 62 | center, 63 | dl, 64 | dt, 65 | dd, 66 | ol, 67 | ul, 68 | li, 69 | fieldset, 70 | form, 71 | label, 72 | legend, 73 | table, 74 | caption, 75 | tbody, 76 | tfoot, 77 | thead, 78 | tr, 79 | th, 80 | td, 81 | article, 82 | aside, 83 | canvas, 84 | details, 85 | embed, 86 | figure, 87 | figcaption, 88 | footer, 89 | header, 90 | hgroup, 91 | menu, 92 | nav, 93 | output, 94 | ruby, 95 | section, 96 | summary, 97 | time, 98 | mark, 99 | audio, 100 | video { 101 | margin: 0; 102 | padding: 0; 103 | border: 0; 104 | font-size: 100%; 105 | vertical-align: baseline; 106 | } 107 | 108 | /* HTML5 display-role reset for older browsers */ 109 | 110 | article, 111 | aside, 112 | details, 113 | figcaption, 114 | figure, 115 | footer, 116 | header, 117 | hgroup, 118 | menu, 119 | nav, 120 | section { 121 | display: block; 122 | } 123 | 124 | body { 125 | line-height: 1.3; 126 | } 127 | 128 | ol, 129 | ul { 130 | list-style: none; 131 | } 132 | 133 | blockquote, 134 | q { 135 | quotes: none; 136 | } 137 | 138 | blockquote:before, 139 | blockquote:after, 140 | q:before, 141 | q:after { 142 | content: ''; 143 | content: none; 144 | } 145 | 146 | table { 147 | border-collapse: collapse; 148 | border-spacing: 0; 149 | } 150 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './App'; 3 | -------------------------------------------------------------------------------- /src/components/AppSettingsModal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './AppSettingsModal'; 3 | -------------------------------------------------------------------------------- /src/components/ApplicationMenu/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ApplicationMenu'; 3 | -------------------------------------------------------------------------------- /src/components/AvailableWidth/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './AvailableWidth'; 3 | -------------------------------------------------------------------------------- /src/components/BigClickableButton/BigClickableButton.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import BigClickableButton, { 5 | Button, 6 | ButtonSideWrapper, 7 | } from './BigClickableButton'; 8 | // TODO: Use Lolex to mock setTimeout - later use Jest mock once PR https://github.com/facebook/jest/pull/5171 landed 9 | import lolex from 'lolex'; 10 | 11 | describe('BigClickableButton component', () => { 12 | let wrapper; 13 | let button; 14 | lolex.install(); // mock setTimeout 15 | 16 | beforeEach(() => { 17 | wrapper = mount(); 18 | button = wrapper.find('button'); 19 | jest.clearAllTimers(); 20 | button.simulate('mouseDown'); 21 | }); 22 | 23 | const createSnapshots = () => { 24 | expect(wrapper.find(Button)).toMatchSnapshot(); 25 | expect(wrapper.find(ButtonSideWrapper)).toMatchSnapshot(); 26 | }; 27 | 28 | it('should render pressed', () => { 29 | expect(wrapper.state('isActive')).toBeTruthy(); 30 | createSnapshots(); 31 | }); 32 | 33 | it('should render released', () => { 34 | button.simulate('mouseUp'); 35 | expect(wrapper.state('isActive')).toBeFalsy(); 36 | createSnapshots(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/BigClickableButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './BigClickableButton'; 3 | -------------------------------------------------------------------------------- /src/components/Button/ButtonBase.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { shallow, mount } from 'enzyme'; 4 | import ButtonBase, { 5 | XSmallButton as xsmall, 6 | SmallButton as small, 7 | MediumButton as medium, 8 | LargeButton as large, 9 | } from './ButtonBase'; 10 | 11 | const sizeComponents = { 12 | xsmall, 13 | small, 14 | medium, 15 | large, 16 | }; 17 | 18 | describe('ButtonBase component', () => { 19 | describe('Button with size prop', () => { 20 | const wrapper = shallow(); 21 | const instance = wrapper.instance(); 22 | 23 | for (const size in sizeComponents) { 24 | it(`should return sized button component for size = ${size}`, () => { 25 | const component = instance.getButtonElem(size); 26 | expect(component).toBe(sizeComponents[size]); 27 | }); 28 | it(`should render button for ${size} size`, () => { 29 | // Using mount so the styles are available in the snapshot 30 | const wrapperSizedButton = mount(); 31 | expect(wrapperSizedButton).toMatchSnapshot(); 32 | }); 33 | it(`should render button for ${size} size with-out padding`, () => { 34 | // Using mount so the styles are available in the snapshot 35 | const wrapperSizedButton = mount(); 36 | expect(wrapperSizedButton).toMatchSnapshot(); 37 | }); 38 | } 39 | 40 | it('should return default size medium', () => { 41 | const componentDefault = instance.getButtonElem(); 42 | expect(componentDefault).toBe(medium); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Button/FillButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import { COLORS, GRADIENTS } from '../../constants'; 5 | 6 | import ButtonBase from './ButtonBase'; 7 | 8 | type Props = { 9 | colors?: Array, 10 | hoverColors?: Array, 11 | textColor: string, 12 | children: React$Node, 13 | disabled?: boolean, 14 | }; 15 | 16 | export const wrapColorsInGradient = (colors?: Array | string) => { 17 | if (!Array.isArray(colors)) { 18 | return colors; 19 | } 20 | 21 | if (colors.length === 1) { 22 | return colors[0]; 23 | } 24 | 25 | return `linear-gradient( 26 | 45deg, 27 | ${colors.join(',')} 28 | )`; 29 | }; 30 | 31 | class FillButton extends Component { 32 | static defaultProps = { 33 | colors: GRADIENTS.primary, 34 | textColor: COLORS.textOnBackground, 35 | }; 36 | 37 | render() { 38 | const { colors, hoverColors, children, ...delegated } = this.props; 39 | 40 | const background = wrapColorsInGradient(colors); 41 | const hoverBackground = wrapColorsInGradient(hoverColors) || background; 42 | 43 | return ( 44 | 50 | {children} 51 | 52 | ); 53 | } 54 | } 55 | 56 | export default FillButton; 57 | -------------------------------------------------------------------------------- /src/components/Button/FillButton.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { decorateAction } from '@storybook/addon-actions'; 5 | import { withInfo } from '@storybook/addon-info'; 6 | 7 | import Showcase from '../../../.storybook/components/Showcase'; 8 | import FillButton from './FillButton'; 9 | 10 | const targetAction = decorateAction([args => [args[0].target]]); 11 | 12 | const SIZES = ['xsmall', 'small', 'medium', 'large']; 13 | 14 | storiesOf('Button / Fill', module) 15 | .add( 16 | 'default', 17 | withInfo()(() => ( 18 | FillButton 19 | )) 20 | ) 21 | .add( 22 | 'sizes', 23 | withInfo()(() => 24 | SIZES.map((size, i) => ( 25 | 26 | 27 | FillButton 28 | 29 | 30 | )) 31 | ) 32 | ) 33 | .add( 34 | 'disabled', 35 | withInfo()(() => ( 36 | 37 | FillButton 38 | 39 | )) 40 | ); 41 | -------------------------------------------------------------------------------- /src/components/Button/FillButton.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import FillButton, { wrapColorsInGradient } from './FillButton'; 5 | import ButtonBase from './ButtonBase'; 6 | 7 | describe('FillButton component', () => { 8 | it('should render button filled', () => { 9 | const wrapper = mount(); 10 | expect(wrapper.find(ButtonBase)).toMatchSnapshot(); 11 | }); 12 | 13 | describe('Wrap colors in gradient', () => { 14 | const testColor = '#fff'; 15 | it('should return single color (string)', () => { 16 | expect(wrapColorsInGradient(testColor)).toEqual(testColor); 17 | }); 18 | it('should return single color (array)', () => { 19 | expect(wrapColorsInGradient([testColor])).toEqual(testColor); 20 | }); 21 | 22 | it('should return a gradient', () => { 23 | const colors = [testColor, '#000']; 24 | expect(wrapColorsInGradient(colors)).toEqual(`linear-gradient( 25 | 45deg, 26 | ${colors.join(',')} 27 | )`); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/components/Button/StrokeButton.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { decorateAction } from '@storybook/addon-actions'; 5 | import { withInfo } from '@storybook/addon-info'; 6 | 7 | import Showcase from '../../../.storybook/components/Showcase'; 8 | import StrokeButton from './StrokeButton'; 9 | 10 | const targetAction = decorateAction([args => [args[0].target]]); 11 | 12 | const SIZES = ['xsmall', 'small', 'medium', 'large']; 13 | 14 | class ToggleableButton extends Component { 15 | state = { 16 | isToggled: true, 17 | }; 18 | 19 | render() { 20 | return ( 21 | this.setState({ isToggled: !this.state.isToggled })} 25 | /> 26 | ); 27 | } 28 | } 29 | 30 | storiesOf('Button / Stroke', module) 31 | .add( 32 | 'default', 33 | withInfo()(() => ( 34 | 35 | StrokeButton 36 | 37 | )) 38 | ) 39 | .add( 40 | 'sizes', 41 | withInfo()(() => 42 | SIZES.map((size, i) => ( 43 | 44 | 45 | StrokeButton 46 | 47 | 48 | )) 49 | ) 50 | ) 51 | .add( 52 | 'disabled', 53 | withInfo()(() => ( 54 | 55 | StrokeButton 56 | 57 | )) 58 | ) 59 | .add( 60 | 'toggleable', 61 | withInfo()(() => I can be toggled!) 62 | ); 63 | -------------------------------------------------------------------------------- /src/components/Button/StrokeButton.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import DetectActive from '../DetectActive'; 5 | import StrokeButton, { Foreground, Background } from './StrokeButton'; 6 | 7 | describe('StrokeButton component', () => { 8 | let wrapper; 9 | let renderProp; 10 | beforeEach(() => { 11 | wrapper = mount(); 12 | renderProp = wrapper.find(DetectActive).renderProp('children'); 13 | }); 14 | 15 | it('should render foreground (inactive)', () => { 16 | expect(renderProp(false).find(Foreground)).toMatchSnapshot(); 17 | }); 18 | it('should render background (inactive)', () => { 19 | expect(renderProp(false).find(Background)).toMatchSnapshot(); 20 | }); 21 | 22 | it('should render foreground (active)', () => { 23 | expect(renderProp(true).find(Foreground)).toMatchSnapshot(); 24 | }); 25 | it('should render background (active)', () => { 26 | expect(renderProp(true).find(Background)).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as StrokeButton } from './StrokeButton'; 3 | export { default as FillButton } from './FillButton'; 4 | 5 | export type { Props as StrokeButtonProps } from './StrokeButton'; 6 | -------------------------------------------------------------------------------- /src/components/ButtonWithIcon/ButtonWithIcon.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { StrokeButton } from '../Button'; 6 | 7 | import type { StrokeButtonProps } from '../Button'; 8 | 9 | type Props = StrokeButtonProps & { 10 | icon: React$Node, 11 | }; 12 | 13 | // Note: noPadding prop seems unavailable on StrokeButton component but 14 | // it is available as it is passed down with delegated props to ButtonBase component. 15 | const ButtonWithIcon = ({ icon, children, ...delegated }: Props) => ( 16 | // TODO: Support other sizes 17 | 18 | 19 | {icon} 20 | {children} 21 | 22 | 23 | ); 24 | 25 | export const InnerWrapper = styled.div` 26 | position: relative; 27 | display: flex; 28 | align-items: center; 29 | height: 34px; 30 | padding: 0 10px 0 32px; 31 | `; 32 | 33 | const IconWrapper = styled.div` 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | right: 0; 38 | width: 34px; 39 | height: 34px; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | `; 44 | 45 | export default ButtonWithIcon; 46 | -------------------------------------------------------------------------------- /src/components/ButtonWithIcon/ButtonWithIcon.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import ButtonWithIcon, { InnerWrapper } from './ButtonWithIcon'; 5 | 6 | describe('ButtonWithIcon component', () => { 7 | it('should render button with icon', () => { 8 | // mock icon to reduce snapshot size 9 | const icon = ; 10 | const wrapper = mount(); 11 | // Snapshot of InnerWrapper is OK as StrokeButton is tested separately. 12 | expect(wrapper.find(InnerWrapper)).toMatchSnapshot(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/ButtonWithIcon/ButtonsWithIcon.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { storiesOf } from '@storybook/react'; 6 | import { action } from '@storybook/addon-actions'; 7 | import { withInfo } from '@storybook/addon-info'; 8 | import { settings } from 'react-icons-kit/feather'; 9 | 10 | import reactIconSrc from '../../assets/images/react-icon.svg'; 11 | 12 | import Showcase from '../../../.storybook/components/Showcase'; 13 | import ButtonWithIcon from './ButtonWithIcon'; 14 | 15 | storiesOf('ButtonWithIcon', module).add( 16 | 'default', 17 | withInfo()(() => ( 18 | 19 | 20 | } 22 | onClick={action('clicked')} 23 | > 24 | Check Button 25 | 26 | 27 | 28 | } 30 | onClick={action('clicked')} 31 | > 32 | Settings 33 | 34 | 35 | 36 | )) 37 | ); 38 | 39 | const ReactIcon = styled.img` 40 | width: 32px; 41 | height: 32px; 42 | `; 43 | -------------------------------------------------------------------------------- /src/components/ButtonWithIcon/__snapshots__/ButtonWithIcon.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ButtonWithIcon component should render button with icon 1`] = ` 4 | .c0 { 5 | position: relative; 6 | display: -webkit-box; 7 | display: -webkit-flex; 8 | display: -ms-flexbox; 9 | display: flex; 10 | -webkit-align-items: center; 11 | -webkit-box-align: center; 12 | -ms-flex-align: center; 13 | align-items: center; 14 | height: 34px; 15 | padding: 0 10px 0 32px; 16 | } 17 | 18 | .c1 { 19 | position: absolute; 20 | top: 0; 21 | left: 0; 22 | right: 0; 23 | width: 34px; 24 | height: 34px; 25 | display: -webkit-box; 26 | display: -webkit-flex; 27 | display: -ms-flexbox; 28 | display: flex; 29 | -webkit-align-items: center; 30 | -webkit-box-align: center; 31 | -ms-flex-align: center; 32 | align-items: center; 33 | -webkit-box-pack: center; 34 | -webkit-justify-content: center; 35 | -ms-flex-pack: center; 36 | justify-content: center; 37 | } 38 | 39 | 40 |
43 | 44 |
47 | 48 |
49 |
50 |
51 |
52 | `; 53 | -------------------------------------------------------------------------------- /src/components/ButtonWithIcon/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ButtonWithIcon'; 3 | -------------------------------------------------------------------------------- /src/components/Card/Card.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | import { COLORS } from '../../constants'; 5 | 6 | export default styled.div` 7 | padding: 15px; 8 | background: ${COLORS.lightBackground}; 9 | border-radius: 8px; 10 | box-shadow: 0px 6px 60px rgba(0, 0, 0, 0.1), 0px 2px 8px rgba(0, 0, 0, 0.05); 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Card'; 3 | -------------------------------------------------------------------------------- /src/components/CircularOutline/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './CircularOutline'; 3 | -------------------------------------------------------------------------------- /src/components/CodesandboxLogo/Logo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | const Logo = ({ 6 | width = 32, 7 | height = 32, 8 | className, 9 | }: { 10 | width: number, 11 | height: number, 12 | className: ?string, 13 | }) => ( 14 | 22 | 23 | 27 | 31 | 35 | 36 | 37 | 44 | 54 | 61 | 62 | 63 | ); 64 | 65 | const StyledLogo = styled(Logo)` 66 | border-radius: 50%; 67 | background: #000; 68 | padding: 5px; 69 | `; 70 | 71 | export default StyledLogo; 72 | -------------------------------------------------------------------------------- /src/components/CodesandboxLogo/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Logo'; 3 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/ImportExisting.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { connect } from 'react-redux'; 4 | import styled from 'styled-components'; 5 | import IconBase from 'react-icons-kit'; 6 | import { folderPlus } from 'react-icons-kit/feather/folderPlus'; 7 | 8 | import { COLORS, RAW_COLORS } from '../../constants'; 9 | 10 | import ImportProjectButton from '../ImportProjectButton'; 11 | 12 | type Props = { 13 | isOnboarding: boolean, 14 | }; 15 | 16 | export const ImportExisting = ({ isOnboarding }: Props) => { 17 | if (isOnboarding) { 18 | // When the user is onboarding, there's a much more prominent prompt to 19 | // import existing projects, so we don't need this extra snippet. 20 | return null; 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Already have a project you'd like to manage with Guppy?{' '} 32 | 33 | Import it instead 34 | . 35 | 36 | 37 | ); 38 | }; 39 | 40 | const Wrapper = styled.div` 41 | display: flex; 42 | align-items: center; 43 | font-size: 1rem; 44 | max-width: 350px; 45 | `; 46 | const IconWrapper = styled.div` 47 | width: 42px; 48 | height: 42px; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | border: 2px solid rgba(255, 255, 255, 0.75); 53 | border-radius: 50%; 54 | color: ${RAW_COLORS.transparentWhite[300]}; 55 | `; 56 | 57 | const MainText = styled.div` 58 | flex: 1; 59 | text-align: left; 60 | margin-left: 10px; 61 | color: ${RAW_COLORS.transparentWhite[300]}; 62 | `; 63 | 64 | const mapStateToProps = state => ({ 65 | isOnboarding: state.onboardingStatus !== 'done', 66 | }); 67 | 68 | export default connect(mapStateToProps)(ImportExisting); 69 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/BuildStepProgress.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import BuildStepProgress from '../BuildStepProgress'; 5 | 6 | describe('BuildStepProgress component', () => { 7 | let wrapper; 8 | let instance; 9 | 10 | const mockStep = { 11 | copy: 'Building...', 12 | additionalCopy: 'This may take some time', 13 | }; 14 | 15 | jest.useFakeTimers(); 16 | 17 | beforeEach(() => { 18 | wrapper = shallow(); 19 | instance = wrapper.instance(); 20 | }); 21 | 22 | it('should render upcoming', () => { 23 | expect(wrapper).toMatchSnapshot(); 24 | }); 25 | 26 | it('should render in-progress', () => { 27 | wrapper.setProps({ status: 'in-progress' }); 28 | expect(wrapper).toMatchSnapshot(); 29 | }); 30 | 31 | it('should render done', () => { 32 | wrapper.setProps({ status: 'done' }); 33 | expect(wrapper).toMatchSnapshot(); 34 | }); 35 | 36 | describe('Component logic', () => { 37 | it('should hide additional copy if done', () => { 38 | wrapper.setState({ 39 | shouldShowAdditionalCopy: true, 40 | }); 41 | wrapper.setProps({ 42 | status: 'done', 43 | }); 44 | expect(instance.state.shouldShowAdditionalCopy).toBeFalsy(); 45 | }); 46 | 47 | it('should show additonal copy after a delay', () => { 48 | wrapper.setProps({ 49 | status: 'in-progress', 50 | }); 51 | jest.runAllTimers(); 52 | expect(instance.state.shouldShowAdditionalCopy).toBeTruthy(); 53 | }); 54 | 55 | it('should clear timeout on unmount', () => { 56 | window.clearTimeout = jest.fn(); 57 | instance.componentWillUnmount(); 58 | expect(window.clearTimeout).toHaveBeenCalledWith(instance.timeoutId); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/ImportExisting.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { ImportExisting } from '../ImportExisting'; 4 | 5 | describe('ImportExisting component', () => { 6 | let wrapper; 7 | 8 | it('should render (after onboarding)', () => { 9 | wrapper = shallow(); 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | 13 | it(`shouldn't render (during onboarding)`, () => { 14 | wrapper = shallow(); 15 | expect(wrapper.html()).toBeNull(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/SubmitButton.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import SubmitButton, { ChildWrapper } from '../SubmitButton'; 4 | 5 | describe('SubmitButton component', () => { 6 | let wrapper; 7 | let mockSubmit; 8 | 9 | const shallowRender = (hasBeenSubmitted, ready, isDisabled = false) => 10 | shallow( 11 | 17 | ); 18 | beforeEach(() => { 19 | mockSubmit = jest.fn(); 20 | }); 21 | 22 | it(`should render 'Building...' button text`, () => { 23 | wrapper = shallowRender(true, true); 24 | expect( 25 | wrapper 26 | .find(ChildWrapper) 27 | .children() 28 | .text() 29 | ).toEqual('Building...'); 30 | }); 31 | 32 | it(`should render 'Lets do this' button text`, () => { 33 | wrapper = shallowRender(false, true); 34 | expect( 35 | wrapper 36 | .find(ChildWrapper) 37 | .children() 38 | .text() 39 | ).toEqual(`Let's do this`); 40 | }); 41 | 42 | it(`should render 'Next' button text`, () => { 43 | wrapper = shallowRender(false, false); 44 | expect( 45 | wrapper 46 | .find(ChildWrapper) 47 | .children() 48 | .text() 49 | ).toEqual(`Next`); 50 | }); 51 | 52 | it('should submit', () => { 53 | wrapper = shallowRender(true, true); 54 | wrapper.simulate('click'); 55 | expect(mockSubmit).toHaveBeenCalled(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/SummaryPane.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import SummaryPane from '../SummaryPane'; 5 | import projectTypes from '../../../config/project-types'; 6 | 7 | describe('SummaryPane component', () => { 8 | let wrapper; 9 | const steps = ['projectName', 'projectType', 'projectIcon', 'projectStarter']; 10 | 11 | steps.forEach(step => 12 | it(`should render summary for ${step}`, () => { 13 | if (step === 'projectType') { 14 | wrapper = shallow(); 15 | Object.keys(projectTypes).forEach(projectType => { 16 | wrapper = shallow( 17 | 18 | ); 19 | expect(wrapper).toMatchSnapshot(); 20 | }); 21 | } else { 22 | wrapper = shallow(); 23 | expect(wrapper).toMatchSnapshot(); 24 | } 25 | }) 26 | ); 27 | 28 | it('should throw an error if step not found', () => { 29 | expect(() => 30 | shallow() 31 | ).toThrow(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/__snapshots__/BuildStepProgress.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BuildStepProgress component should render done 1`] = ` 4 | 7 | 8 | 32 | 33 | 34 | 35 | Building... 36 | 37 | 38 | 39 | `; 40 | 41 | exports[`BuildStepProgress component should render in-progress 1`] = ` 42 | 45 | 46 | 49 | 50 | 51 | 52 | Building... 53 | 54 | 55 | 56 | `; 57 | 58 | exports[`BuildStepProgress component should render upcoming 1`] = ` 59 | 62 | 63 | 64 | 65 | Building... 66 | 67 | 68 | 69 | `; 70 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/__snapshots__/CreateNewProjectWizard.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CreateNewProjectWizard component should render TwoPaneModal 1`] = ` 4 | 5 | 15 | } 16 | isDismissable={true} 17 | isFolded={false} 18 | leftPane={ 19 | 23 | 27 | 28 | } 29 | onDismiss={[MockFunction]} 30 | rightPane={ 31 | 44 | } 45 | transitionState={true} 46 | /> 47 | 48 | `; 49 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/__snapshots__/ImportExisting.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ImportExisting component should render (after onboarding) 1`] = ` 4 | 5 | 6 | 7 | 52 | 53 | 54 | 55 | Already have a project you'd like to manage with Guppy? 56 | 57 | 60 | Import it instead 61 | 62 | . 63 | 64 | 65 | `; 66 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/__tests__/helpers.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import { 3 | replaceProjectStarterStringWithUrl, 4 | defaultStarterUrl, 5 | } from '../helpers'; 6 | 7 | describe('Build helpers', () => { 8 | describe('Gatsby helper', () => { 9 | it('should replace Gatsby starter string with url', () => { 10 | expect(replaceProjectStarterStringWithUrl('gatsby-starter-blog')).toEqual( 11 | defaultStarterUrl + 'gatsby-starter-blog' 12 | ); 13 | }); 14 | 15 | it('should ignore empty starter strings', () => { 16 | expect(replaceProjectStarterStringWithUrl('')).toEqual(''); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // Not perfect to have this helper but we're having two locations (BuildPane & CreateNewProjectWizard) where it is used 4 | // I'd like to keep the user input unmodified and use only Gatsby github repo as short-hand so entering 5 | // gatsby-starter-blog will be https://github.com/gatsby/gatsby-starter-blog 6 | // Todo: We could also add a short-hand for username/repo to replace with https://github.com/username/repo 7 | // An additional check for string with-out slash would be required to still support the Gatsby replacement. 8 | 9 | export const defaultStarterUrl = 'https://github.com/gatsbyjs/'; 10 | 11 | export const replaceProjectStarterStringWithUrl = ( 12 | projectStarterInput: string 13 | ) => 14 | !projectStarterInput.includes('http') && projectStarterInput !== '' 15 | ? defaultStarterUrl + projectStarterInput 16 | : projectStarterInput; 17 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './CreateNewProjectWizard'; 3 | -------------------------------------------------------------------------------- /src/components/CreateNewProjectWizard/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type Field = 4 | | 'projectName' 5 | | 'projectType' 6 | | 'projectIcon' 7 | | 'projectStarter'; 8 | export type BuildStep = 9 | | 'installingCliTool' 10 | | 'creatingProjectDirectory' 11 | | 'installingDependencies' 12 | | 'guppification'; 13 | 14 | export type Status = 'filling-in-form' | 'building-project' | 'project-created'; 15 | 16 | export type Step = Field | BuildStep; 17 | -------------------------------------------------------------------------------- /src/components/CustomHighlight/CustomHighlight.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { connectHighlight } from 'react-instantsearch/connectors'; 4 | 5 | import { safeEscapeString } from '../../utils'; 6 | 7 | type Props = any; 8 | 9 | // A CustomHighlight component to use with react-instantsearch 10 | // to ensure elements are properly unescaped and spaced on output 11 | const CustomHighlight = ({ 12 | highlight, 13 | attribute, 14 | hit, 15 | highlightProperty, 16 | }: Props) => { 17 | const highlights = highlight({ 18 | highlightProperty: '_highlightResult', 19 | attribute, 20 | hit, 21 | }); 22 | 23 | return highlights.map((part, i) => { 24 | // Run the DOMParser on each of the parts of text 25 | const unescaped = safeEscapeString(part.value); 26 | 27 | // If the text is highlighted, wrap in for the highlight 28 | // Otherwise just render the part in a 29 | return part.isHighlighted ? ( 30 | 31 | {unescaped} 32 | 33 | ) : ( 34 | {unescaped} 35 | ); 36 | }); 37 | }; 38 | 39 | export default connectHighlight(CustomHighlight); 40 | -------------------------------------------------------------------------------- /src/components/CustomHighlight/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './CustomHighlight'; 3 | -------------------------------------------------------------------------------- /src/components/Debounced/Debounced.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Utility component that delays rendering its children, to prevent against 4 | * "quick flashes". 5 | * 6 | * Used in the Onboarding Wizard to prevent the SummaryPane from updating 7 | * when the user clicks the "randomize" button (clicking on the button blurs 8 | * focus from the Project Name field, but then it immediately resets it. Without 9 | * the debounce, it causes the Project Name summary to flash) 10 | */ 11 | import { Component } from 'react'; 12 | 13 | type Props = { 14 | on: any, 15 | duration: number, 16 | children: React$Node, 17 | }; 18 | 19 | type State = { 20 | props: any, 21 | children?: React$Node, 22 | }; 23 | 24 | class Debounced extends Component { 25 | static defaultProps = { 26 | duration: 100, 27 | }; 28 | 29 | state = { props: this.props }; 30 | 31 | timeoutId: ?number; 32 | 33 | componentWillReceiveProps(nextProps: Props) { 34 | const { duration } = this.props; 35 | 36 | window.clearTimeout(this.timeoutId); 37 | 38 | this.timeoutId = window.setTimeout(() => { 39 | this.setState({ props: nextProps }); 40 | }, duration); 41 | } 42 | 43 | shouldComponentUpdate(prevProps: Props, prevState: State) { 44 | return this.props !== this.state.props; 45 | } 46 | 47 | componentWillUnmount() { 48 | window.clearTimeout(this.timeoutId); 49 | } 50 | 51 | render() { 52 | return this.props.children; 53 | } 54 | } 55 | 56 | export default Debounced; 57 | -------------------------------------------------------------------------------- /src/components/Debounced/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Debounced'; 3 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/AddDependencyInitialScreen.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { COLORS } from '../../constants'; 6 | 7 | import Paragraph from '../Paragraph'; 8 | import ExternalLink from '../ExternalLink'; 9 | import AlgoliaLogo from './AlgoliaLogo'; 10 | 11 | type Props = {}; 12 | 13 | class AddDependencyInitialScreen extends Component { 14 | render() { 15 | return ( 16 | 17 | 18 | You can use the input above to search the Node Package Manager (NPM) 19 | registry for packages that have been published. 20 | 21 | 22 | Search by package name, description, keyword, or author. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | export const InstructionsParagraph = Paragraph.extend` 37 | font-size: 1.4rem; 38 | color: ${COLORS.lightText}; 39 | `; 40 | 41 | const EmptyState = styled.div` 42 | width: 100%; 43 | height: 100%; 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: space-around; 47 | padding: 100px 40px; 48 | text-align: center; 49 | `; 50 | 51 | export const PoweredByWrapper = styled.div` 52 | position: absolute; 53 | left: 0; 54 | right: 0; 55 | bottom: 25px; 56 | `; 57 | 58 | const LinkText = styled.div` 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | text-align: center; 63 | color: ${COLORS.lightText}; 64 | `; 65 | 66 | export default AddDependencyInitialScreen; 67 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/AddDependencySearchBox.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This component uses Algolia's `react-instantsearch`. 4 | * Unfortunately, styling these components is tricky, and doesn't play nicely 5 | * with styled-components. Just gonna override the provided global styles. 6 | */ 7 | import React, { Component } from 'react'; 8 | import styled, { injectGlobal } from 'styled-components'; 9 | import { SearchBox } from 'react-instantsearch/dom'; 10 | 11 | import { COLORS } from '../../constants'; 12 | 13 | type Props = { 14 | onChange?: (ev: SyntheticEvent<*>) => void, 15 | }; 16 | 17 | class AddDependencySearchBox extends Component { 18 | render() { 19 | return ( 20 | 21 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | const Wrapper = styled.div` 32 | padding-top: 15px; 33 | width: 100%; 34 | `; 35 | 36 | injectGlobal` 37 | .ais-SearchBox { 38 | /* width: 100%; */ 39 | } 40 | .ais-SearchBox-submit, .ais-SearchBox-reset { 41 | display: none; 42 | } 43 | .ais-SearchBox-input { 44 | width: 100%; 45 | padding: 8px 0px; 46 | background: transparent; 47 | border: none; 48 | border-bottom: 2px solid rgba(255, 255, 255, 0.75); 49 | color: ${COLORS.lightBackground}; 50 | border-radius: 0px; 51 | outline: none; 52 | font-size: 21px; 53 | 54 | &::placeholder { 55 | color: rgba(255, 255, 255, 0.35); 56 | } 57 | 58 | &:focus { 59 | border-bottom: 2px solid ${COLORS.lightBackground}; 60 | 61 | } 62 | } 63 | `; 64 | 65 | export default AddDependencySearchBox; 66 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/AddDependencySearchProvider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { InstantSearch, Configure } from 'react-instantsearch/dom'; 4 | 5 | import { ALGOLIA_KEYS } from '../../constants'; 6 | 7 | type Props = { 8 | children: React$Node, 9 | }; 10 | 11 | class AddDependencySearchProvider extends Component { 12 | render() { 13 | return ( 14 | 15 | 27 | {this.props.children} 28 | 29 | ); 30 | } 31 | } 32 | 33 | export default AddDependencySearchProvider; 34 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/DependencyInstalling.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, Fragment } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import guppyLoaderSrc from '../../assets/images/guppy-loader.gif'; 6 | import { RAW_COLORS } from '../../constants'; 7 | 8 | import Heading from '../Heading'; 9 | import Spacer from '../Spacer'; 10 | 11 | type Props = { 12 | name: string, 13 | queued: boolean, 14 | }; 15 | 16 | class DependencyInstalling extends Component { 17 | render() { 18 | const { name, queued } = this.props; 19 | const stylizedName = ( 20 | {name} 21 | ); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {queued ? ( 30 | {stylizedName} is queued for install... 31 | ) : ( 32 | Installing {stylizedName}... 33 | )} 34 | 35 | 36 | 37 | ); 38 | } 39 | } 40 | 41 | const Wrapper = styled.div` 42 | width: 100%; 43 | height: 100%; 44 | display: flex; 45 | flex-direction: column; 46 | justify-content: center; 47 | align-items: center; 48 | `; 49 | 50 | const InnerWrapper = styled.div` 51 | text-align: center; 52 | `; 53 | 54 | const GuppyImage = styled.img` 55 | width: 148px; 56 | `; 57 | 58 | export default DependencyInstalling; 59 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__mocks__/dependency.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const mockReactHit = { 3 | name: 'React', 4 | version: '16.0.0', 5 | license: 'MIT', 6 | modified: 1549828927372, 7 | humanDownloadsLast30Days: 1000000, 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/AddDependencyInitialScreen.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import AddDependencyInitialScreen, { 5 | InstructionsParagraph, 6 | PoweredByWrapper, 7 | } from '../AddDependencyInitialScreen'; 8 | 9 | jest.mock('../AlgoliaLogo', () => 'svg'); 10 | 11 | describe('AddDependencyInitialScreen component', () => { 12 | let wrapper; 13 | beforeEach(() => { 14 | wrapper = mount(); 15 | }); 16 | 17 | it('should render instruction paragraphs', () => { 18 | expect(wrapper.find(InstructionsParagraph)).toMatchSnapshot(); 19 | }); 20 | 21 | it('should render powered by Algolia link', () => { 22 | expect(wrapper.find(PoweredByWrapper)).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/AddDependencyModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { InfiniteHits } from 'react-instantsearch/dom'; 4 | 5 | import { AddDependencyModal } from '../AddDependencyModal'; 6 | import AddDependencyInitialScreen from '../AddDependencyInitialScreen'; 7 | 8 | describe('AddDependencyModal component', () => { 9 | let wrapper; 10 | beforeEach(() => { 11 | wrapper = shallow(); 12 | }); 13 | 14 | it('should render initial screen', () => { 15 | expect(wrapper.find(AddDependencyInitialScreen)).toHaveLength(1); 16 | }); 17 | 18 | it('should render hits', () => { 19 | wrapper.setProps({ 20 | searchResults: { 21 | query: 'React', 22 | }, 23 | }); 24 | expect(wrapper.find(InfiniteHits)).toHaveLength(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/AddDependencySearchBox.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import AddDependencySearchBox from '../AddDependencySearchBox'; 5 | 6 | describe('AddDependencySearchBox component', () => { 7 | const wrapper = shallow(); 8 | 9 | it('should render SearchBox', () => { 10 | expect(wrapper).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/AddDependencySearchProvider.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import AddDependencySearchProvider from '../AddDependencySearchProvider'; 5 | 6 | describe('AddDependencySearchProvider component', () => { 7 | const wrapper = shallow( 8 | 9 |
10 | 11 | ); 12 | 13 | it('should render SearchBox', () => { 14 | expect(wrapper).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/DependencyDetails.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import lolex from 'lolex'; 4 | 5 | import DependencyDetails from '../DependencyDetails'; 6 | import { mockReactHit } from '../__mocks__/dependency'; 7 | 8 | describe('DependencyDetails component', () => { 9 | lolex.install(); 10 | 11 | const projectId = 'a-project'; 12 | const wrapper = shallow( 13 | 14 | ); 15 | 16 | it('should render', () => { 17 | expect( 18 | wrapper.renderProp('children')({ 19 | name: mockReactHit.name, 20 | latestVersion: '16.8.0', 21 | lastUpdatedAt: Date.now(), 22 | isLoading: false, 23 | }) 24 | ).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/DependencyInstalling.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | import DependencyInstalling from '../DependencyInstalling'; 5 | import { mockReactHit } from '../__mocks__/dependency'; 6 | 7 | describe('DependencyInstalling component', () => { 8 | const wrapper = shallow(); 9 | 10 | it('should render (queued)', () => { 11 | wrapper.setProps({ 12 | queued: true, 13 | }); 14 | expect(wrapper).toMatchSnapshot(); 15 | }); 16 | 17 | it('should render (installing)', () => { 18 | wrapper.setProps({ 19 | queued: false, 20 | }); 21 | expect(wrapper).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/__snapshots__/AddDependencySearchBox.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AddDependencySearchBox component should render SearchBox 1`] = ` 4 | 5 | 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/__snapshots__/AddDependencySearchProvider.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AddDependencySearchProvider component should render SearchBox 1`] = ` 4 | 18 | 37 |
38 | 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/__snapshots__/DependencyDetails.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DependencyDetails component should render 1`] = ` 4 | 5 | 6 | 10 | 11 | 14 | React 15 | 16 | 17 | 18 | 19 | 20 | 21 | 35 | 36 | 37 | 50 | 51 | 52 | `; 53 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/__tests__/__snapshots__/DependencyInstalling.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DependencyInstalling component should render (installing) 1`] = ` 4 | 5 | 6 | 9 | 12 | 15 | Installing 16 | 23 | React 24 | 25 | ... 26 | 27 | 28 | 29 | `; 30 | 31 | exports[`DependencyInstalling component should render (queued) 1`] = ` 32 | 33 | 34 | 37 | 40 | 43 | 50 | React 51 | 52 | is queued for install... 53 | 54 | 55 | 56 | `; 57 | -------------------------------------------------------------------------------- /src/components/DependencyManagementPane/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './DependencyManagementPane'; 3 | -------------------------------------------------------------------------------- /src/components/DetectActive/DetectActive.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | className: string, 6 | children: (isActive: boolean, isHovered: boolean) => React$Node, 7 | }; 8 | 9 | type State = { 10 | isActive: boolean, 11 | isHovered: boolean, 12 | }; 13 | 14 | class DetectActive extends Component { 15 | static defaultProps = { 16 | className: '', 17 | }; 18 | 19 | state = { 20 | isActive: false, 21 | isHovered: false, 22 | }; 23 | 24 | handleMouseDown = (ev: SyntheticEvent<*>) => { 25 | this.setState({ isActive: true }); 26 | }; 27 | 28 | handleMouseUp = (ev: SyntheticEvent<*>) => { 29 | this.setState({ isActive: false }); 30 | }; 31 | 32 | handleMouseOver = (ev: SyntheticEvent<*>) => { 33 | this.setState({ isHovered: true }); 34 | }; 35 | 36 | handleMouseLeave = (ev: SyntheticEvent<*>) => { 37 | this.setState({ isActive: false, isHovered: false }); 38 | }; 39 | 40 | render() { 41 | const { className } = this.props; 42 | return ( 43 | 50 | {this.props.children(this.state.isActive, this.state.isHovered)} 51 | 52 | ); 53 | } 54 | } 55 | 56 | export default DetectActive; 57 | -------------------------------------------------------------------------------- /src/components/DetectActive/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './DetectActive'; 3 | -------------------------------------------------------------------------------- /src/components/DevTools/index.dev.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { createDevTools } from 'redux-devtools'; 4 | import LogMonitor from 'redux-devtools-log-monitor'; 5 | import DockMonitor from 'redux-devtools-dock-monitor'; 6 | 7 | const DevTools = createDevTools( 8 | 13 | 14 | 15 | ); 16 | 17 | export default DevTools; 18 | -------------------------------------------------------------------------------- /src/components/DevTools/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable no-undef */ 3 | if (process.env.NODE_ENV === 'production') { 4 | // eslint-disable-next-line global-require 5 | module.exports = require('./index.prod').default; 6 | } else { 7 | // eslint-disable-next-line global-require 8 | module.exports = require('./index.dev').default; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/DevTools/index.prod.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | const DevTools = () =>
; 5 | DevTools.instrument = () => {}; 6 | 7 | export default DevTools; 8 | -------------------------------------------------------------------------------- /src/components/DevelopmentServerPane/DevelopmentServerPane.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | import lolex from 'lolex'; 5 | 6 | import { DevelopmentServerPane } from './DevelopmentServerPane'; 7 | 8 | describe('DevelopmentServerPane component', () => { 9 | let wrapper; 10 | let instance; 11 | lolex.install(); 12 | 13 | const task = { 14 | name: 'start', 15 | status: 'idle', 16 | }; 17 | 18 | const project = { 19 | id: 'a-project', 20 | type: 'create-react-app', 21 | }; 22 | 23 | const mockActions = { 24 | launchDevServer: jest.fn(), 25 | abortTask: jest.fn(), 26 | }; 27 | 28 | beforeEach(() => { 29 | wrapper = shallow( 30 | 31 | ); 32 | instance = wrapper.instance(); 33 | }); 34 | 35 | describe('Rendering', () => { 36 | it('should render', () => { 37 | expect(wrapper).toMatchSnapshot(); 38 | }); 39 | 40 | it('should return message if no tasks', () => { 41 | wrapper = shallow(); 42 | expect(wrapper.text()).toMatch(/This project does not appear/); 43 | }); 44 | }); 45 | 46 | describe('Component logic', () => { 47 | it('should start devServer', () => { 48 | instance.handleToggle(true); 49 | expect(mockActions.launchDevServer).toHaveBeenCalledWith( 50 | task, 51 | new Date() 52 | ); 53 | }); 54 | 55 | it('should abort task', () => { 56 | instance.handleToggle(false); 57 | expect(mockActions.abortTask).toHaveBeenCalledWith( 58 | task, 59 | project.type, 60 | new Date() 61 | ); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/DevelopmentServerPane/__snapshots__/DevelopmentServerPane.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DevelopmentServerPane component Rendering should render 1`] = ` 4 | 13 | } 14 | title="Development Server" 15 | > 16 | 20 | 21 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/DevelopmentServerPane/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './DevelopmentServerPane'; 3 | -------------------------------------------------------------------------------- /src/components/DevelopmentServerStatus/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './DevelopmentServerStatus'; 3 | -------------------------------------------------------------------------------- /src/components/DirectoryPicker/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './DirectoryPicker'; 3 | -------------------------------------------------------------------------------- /src/components/Divider/Divider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Used in AddDependencySearchResult & StarterSelection 3 | import styled from 'styled-components'; 4 | 5 | import { RAW_COLORS } from '../../constants'; 6 | 7 | const Divider = styled.div` 8 | width: 100%; 9 | height: 1px; 10 | background: ${RAW_COLORS.gray[100]}; 11 | `; 12 | 13 | export default Divider; 14 | -------------------------------------------------------------------------------- /src/components/Divider/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Divider'; 3 | -------------------------------------------------------------------------------- /src/components/Earth/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Earth'; 3 | -------------------------------------------------------------------------------- /src/components/EjectButton/EjectButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import IconBase from 'react-icons-kit'; 4 | import { ic_eject as ejectIcon } from 'react-icons-kit/md/ic_eject'; 5 | import { remote } from 'electron'; 6 | 7 | import BigClickableButton from '../BigClickableButton'; 8 | 9 | const { dialog } = remote; 10 | 11 | type Props = { 12 | width: number, 13 | height: number, 14 | isRunning: boolean, 15 | onClick: () => void, 16 | }; 17 | 18 | export const dialogOptions = { 19 | type: 'warning', 20 | buttons: ['Yes, light this candle', "Ahhh no don't do that"], 21 | defaultId: 1, 22 | cancelId: 1, 23 | title: 'Are you sure?', 24 | message: 25 | 'Ejecting is a permanent one-time task that unwraps the create-react-app environment.', 26 | detail: 27 | "It's recommended for users comfortable with Webpack, who need to make tweaks not possible without ejecting.", 28 | }; 29 | 30 | export function dialogCallback(response: number) { 31 | // The response will be the index of the chosen option, from the 32 | // `buttons` array above. 33 | const isConfirmed = response === 0; 34 | 35 | if (isConfirmed) { 36 | this.props.onClick(); 37 | } 38 | } 39 | 40 | class EjectButton extends PureComponent { 41 | render() { 42 | const { isRunning } = this.props; 43 | 44 | return ( 45 | 50 | dialog.showMessageBox(dialogOptions, dialogCallback.bind(this)) 51 | } 52 | > 53 | 54 | 55 | ); 56 | } 57 | } 58 | 59 | export default EjectButton; 60 | -------------------------------------------------------------------------------- /src/components/EjectButton/EjectButton.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | import { remote } from 'electron'; // Mocked 5 | import EjectButton, { dialogOptions, dialogCallback } from './EjectButton'; 6 | import BigClickableButton from '../BigClickableButton'; 7 | 8 | const { dialog } = remote; 9 | 10 | describe('EjectButton component', () => { 11 | let wrapper; 12 | let clickHandler; 13 | 14 | beforeEach(() => { 15 | clickHandler = jest.fn(); 16 | wrapper = shallow(); 17 | }); 18 | 19 | it('should render without being pressed (not running)', () => { 20 | expect(wrapper).toMatchSnapshot(); 21 | }); 22 | 23 | it('should render as pressed (running)', () => { 24 | wrapper.setProps({ isRunning: true }); 25 | expect(wrapper).toMatchSnapshot(); 26 | }); 27 | 28 | describe('Confirm dialog', () => { 29 | let ejectButton; 30 | beforeEach(() => { 31 | ejectButton = wrapper.find(BigClickableButton); 32 | ejectButton.simulate('click'); 33 | }); 34 | 35 | it('should display dialog', () => { 36 | expect(dialog.showMessageBox).toHaveBeenCalledWith( 37 | dialogOptions, 38 | expect.anything() 39 | ); 40 | }); 41 | 42 | it('should call clickHandler if confirmed', () => { 43 | dialogCallback.call(wrapper.instance(), 0); 44 | expect(clickHandler.mock.calls.length).toBe(1); 45 | }); 46 | 47 | it('should dismiss with-out calling clickHandler', () => { 48 | dialogCallback.call(wrapper.instance(), 1); 49 | expect(clickHandler.mock.calls.length).toBe(0); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/EjectButton/__snapshots__/EjectButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EjectButton component should render as pressed (running) 1`] = ` 4 | 17 | 34 | 35 | `; 36 | 37 | exports[`EjectButton component should render without being pressed (not running) 1`] = ` 38 | 50 | 67 | 68 | `; 69 | -------------------------------------------------------------------------------- /src/components/EjectButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './EjectButton'; 3 | -------------------------------------------------------------------------------- /src/components/ExternalLink/ExternalLink.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | import { shell } from 'electron'; 5 | 6 | import { COLORS } from '../../constants'; 7 | 8 | type Props = { 9 | href: string | null, 10 | children: React$Node, 11 | color: string, 12 | hoverColor?: string, 13 | showUnderline?: boolean, 14 | display: string, 15 | }; 16 | 17 | class ExternalLink extends Component { 18 | static defaultProps = { 19 | color: COLORS.link, 20 | hoverColor: COLORS.lightLink, 21 | display: 'inline-block', 22 | }; 23 | 24 | handleClick = (ev: SyntheticMouseEvent<*>) => { 25 | const { href } = this.props; 26 | 27 | ev.preventDefault(); 28 | shell.openExternal(href); 29 | }; 30 | 31 | render() { 32 | const { href, color, hoverColor, children, display } = this.props; 33 | 34 | return ( 35 | 42 | {children} 43 | 44 | ); 45 | } 46 | } 47 | 48 | const Anchor = styled.a` 49 | position: relative; 50 | text-decoration: none; 51 | color: ${props => props.color}; 52 | 53 | &:hover { 54 | color: ${props => props.hoverColor}; 55 | } 56 | `; 57 | 58 | export default ExternalLink; 59 | -------------------------------------------------------------------------------- /src/components/ExternalLink/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ExternalLink'; 3 | -------------------------------------------------------------------------------- /src/components/FadeIn/FadeIn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | const fadeIn = keyframes` 5 | from { opacity: 0 } 6 | to { opacity: 1 } 7 | `; 8 | 9 | const FadeIn = styled.div` 10 | animation: ${fadeIn} ${props => props.duration || 500}ms; 11 | `; 12 | 13 | export default FadeIn; 14 | -------------------------------------------------------------------------------- /src/components/FadeIn/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './FadeIn'; 3 | -------------------------------------------------------------------------------- /src/components/FadeOnChange/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './FadeOnChange'; 3 | -------------------------------------------------------------------------------- /src/components/FeedbackButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './FeedbackButton'; 3 | -------------------------------------------------------------------------------- /src/components/FormField/FormField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { RAW_COLORS, COLORS } from '../../constants'; 6 | 7 | import Label from '../Label'; 8 | 9 | type Props = { 10 | label: string, 11 | useLabelTag?: boolean, 12 | isFocused?: boolean, 13 | hasError?: boolean, 14 | children: React$Node, 15 | spacing: number, 16 | }; 17 | 18 | class FormField extends PureComponent { 19 | static defaultProps = { 20 | spacing: 30, 21 | }; 22 | 23 | render() { 24 | const { 25 | label, 26 | useLabelTag, 27 | isFocused, 28 | hasError, 29 | children, 30 | spacing, 31 | } = this.props; 32 | 33 | const Wrapper = useLabelTag ? WrapperLabel : WrapperDiv; 34 | 35 | return ( 36 | 37 | 38 | {label} 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | } 45 | 46 | const getTextColor = (props: Props) => { 47 | if (props.hasError) { 48 | return COLORS.lightError; 49 | } else if (props.isFocused) { 50 | return RAW_COLORS.purple[700]; 51 | } else { 52 | return RAW_COLORS.gray[500]; 53 | } 54 | }; 55 | 56 | const WrapperLabel = styled.label` 57 | display: block; 58 | margin-bottom: ${props => props.spacing}px; 59 | `; 60 | 61 | const WrapperDiv = styled.div` 62 | margin-bottom: ${props => props.spacing}px; 63 | `; 64 | 65 | const LabelText = styled(Label)` 66 | flex-basis: ${props => props.width}px; 67 | color: ${getTextColor}; 68 | padding: 0px 5px; 69 | `; 70 | 71 | const Children = styled.div` 72 | flex: 1; 73 | padding: 0px 5px 5px; 74 | `; 75 | 76 | export default FormField; 77 | -------------------------------------------------------------------------------- /src/components/FormField/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './FormField'; 3 | -------------------------------------------------------------------------------- /src/components/FullWidth/FullWidth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Utility component that lets its children take up the full screen width, 4 | * even when within a fixed-width parent. 5 | */ 6 | import styled from 'styled-components'; 7 | 8 | export default styled.div` 9 | width: 100vw; 10 | position: relative; 11 | left: 50%; 12 | right: 50%; 13 | margin-left: -50vw; 14 | margin-right: -50vw; 15 | `; 16 | -------------------------------------------------------------------------------- /src/components/FullWidth/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './FullWidth'; 3 | -------------------------------------------------------------------------------- /src/components/Heading/Heading.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { RAW_COLORS } from '../../constants'; 6 | 7 | type Props = { 8 | size: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge', 9 | children: React$Node, 10 | }; 11 | 12 | class Heading extends Component { 13 | static defaultProps = { 14 | size: 'medium', 15 | }; 16 | render() { 17 | const { size, ...delegated } = this.props; 18 | 19 | switch (this.props.size) { 20 | case 'xsmall': 21 | return ; 22 | case 'small': 23 | return ; 24 | case 'medium': 25 | default: 26 | return ; 27 | case 'large': 28 | return ; 29 | case 'xlarge': 30 | return ; 31 | } 32 | } 33 | } 34 | 35 | const HeadingXSmall = styled.h6` 36 | font-size: 21px; 37 | font-weight: 600; 38 | -webkit-font-smoothing: antialiased; 39 | text-rendering: optimizeLegibility; 40 | color: ${RAW_COLORS.gray[800]}; 41 | `; 42 | 43 | const HeadingSmall = HeadingXSmall.withComponent('h3').extend` 44 | font-size: 26px; 45 | `; 46 | 47 | const HeadingMedium = HeadingSmall.withComponent('h3').extend` 48 | font-size: 32px; 49 | letter-spacing: -0.5px; 50 | `; 51 | 52 | const HeadingLarge = HeadingSmall.withComponent('h1').extend` 53 | font-size: 42px; 54 | letter-spacing: -1px; 55 | `; 56 | 57 | const HeadingXLarge = HeadingSmall.withComponent('h1').extend` 58 | font-size: 60px; 59 | letter-spacing: -2px; 60 | `; 61 | 62 | export default Heading; 63 | -------------------------------------------------------------------------------- /src/components/Heading/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Heading'; 3 | -------------------------------------------------------------------------------- /src/components/HelpButton/HelpButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { u2753 as questionMarkIcon } from 'react-icons-kit/noto_emoji_regular/u2753'; 6 | import { shell } from 'electron'; 7 | 8 | import { RAW_COLORS, COLORS } from '../../constants'; 9 | 10 | type Props = { 11 | size?: number, 12 | href: string, 13 | }; 14 | 15 | const HelpButton = ({ size = 18, href }: Props) => ( 16 | { 19 | shell.openExternal(href); 20 | }} 21 | > 22 | 23 | 24 | ); 25 | 26 | const Help = styled.button` 27 | width: ${props => props.size}px; 28 | height: ${props => props.size}px; 29 | margin-left: 15px; 30 | display: inline-flex; 31 | justify-content: center; 32 | align-items: center; 33 | border: none; 34 | border-radius: 50%; 35 | color: ${COLORS.textOnBackground}; 36 | background: ${RAW_COLORS.gray[500]}; 37 | padding: 0; 38 | cursor: pointer; 39 | 40 | &:hover { 41 | background: ${COLORS.link}; 42 | } 43 | `; 44 | 45 | export default HelpButton; 46 | -------------------------------------------------------------------------------- /src/components/HelpButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './HelpButton'; 3 | -------------------------------------------------------------------------------- /src/components/HoverableOutlineButton/HoverableOutlineButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { StrokeButton } from '../Button'; 4 | 5 | type Props = { 6 | handleMouseEnter?: () => void, 7 | handleMouseLeave?: () => void, 8 | children: React$Node, 9 | }; 10 | 11 | type State = { 12 | isHovered: boolean, 13 | }; 14 | 15 | class HoverableOutlineButton extends Component { 16 | state = { 17 | isHovered: false, 18 | }; 19 | 20 | handleMouseEnter = () => { 21 | this.setState({ isHovered: true }); 22 | this.props.handleMouseEnter && this.props.handleMouseEnter(); 23 | }; 24 | handleMouseLeave = () => { 25 | this.setState({ isHovered: false }); 26 | this.props.handleMouseLeave && this.props.handleMouseLeave(); 27 | }; 28 | 29 | render() { 30 | const { children, ...delegated } = this.props; 31 | const { isHovered } = this.state; 32 | 33 | return ( 34 | 40 | {children} 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default HoverableOutlineButton; 47 | -------------------------------------------------------------------------------- /src/components/HoverableOutlineButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './HoverableOutlineButton'; 3 | -------------------------------------------------------------------------------- /src/components/Icons/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import reactIconSrc from '../../assets/images/react-icon.svg'; 5 | import gatsbyIconSrc from '../../assets/images/gatsby_small.png'; 6 | import nextjsIconSrc from '../../assets/images/nextjs_small.png'; 7 | 8 | const Icon = styled.img` 9 | width: 22px; 10 | height: 22px; 11 | `; 12 | 13 | const SizedReactIcon = styled.img` 14 | width: 32px; 15 | height: 32px; 16 | `; 17 | 18 | const ReactIcon = ; 19 | 20 | const GatsbyIcon = ; 21 | 22 | const NextjsIcon = ; 23 | 24 | export { GatsbyIcon, NextjsIcon, ReactIcon }; 25 | -------------------------------------------------------------------------------- /src/components/ImportProjectButton/ImportProjectButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as actions from '../../actions'; 6 | 7 | import TextButton from '../TextButton'; 8 | 9 | import type { Dispatch } from '../../actions/types'; 10 | 11 | type Props = { 12 | color: string, 13 | children: React$Node, 14 | showImportExistingProjectPrompt: Dispatch< 15 | typeof actions.showImportExistingProjectPrompt 16 | >, 17 | }; 18 | 19 | class ImportProjectButton extends Component { 20 | render() { 21 | const { color, children, showImportExistingProjectPrompt } = this.props; 22 | 23 | return ( 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | } 30 | 31 | export default connect( 32 | null, 33 | { showImportExistingProjectPrompt: actions.showImportExistingProjectPrompt } 34 | )(ImportProjectButton); 35 | -------------------------------------------------------------------------------- /src/components/ImportProjectButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ImportProjectButton'; 3 | -------------------------------------------------------------------------------- /src/components/Initialization/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Initialization'; 3 | -------------------------------------------------------------------------------- /src/components/IntroScreen/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './IntroScreen'; 3 | -------------------------------------------------------------------------------- /src/components/Label/Label.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | export default styled.div` 5 | font-size: 13px; 6 | text-transform: uppercase; 7 | font-weight: 600; 8 | -webkit-font-smoothing: antialiased; 9 | `; 10 | -------------------------------------------------------------------------------- /src/components/Label/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Label'; 3 | -------------------------------------------------------------------------------- /src/components/LargeLED/LargeLED.helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Color from 'color'; 3 | 4 | import { RAW_COLORS, COLORS } from '../../constants'; 5 | 6 | import type { TaskStatus } from '../../types'; 7 | 8 | export type ColorData = { 9 | base: string, 10 | highlight: string, 11 | pulseBase: string, 12 | pulseHighlight: string, 13 | shadowLight: string, 14 | shadowDark: string, 15 | }; 16 | 17 | export const getColorsForStatus = (status: TaskStatus): ColorData => { 18 | switch (status) { 19 | case 'success': { 20 | return { 21 | base: COLORS.lightSuccess, 22 | highlight: RAW_COLORS.lime[500], 23 | pulseBase: COLORS.success, 24 | pulseHighlight: RAW_COLORS.lime[500], 25 | shadowLight: Color(RAW_COLORS.green[900]) 26 | .alpha(0.25) 27 | .rgb() 28 | .string(), 29 | shadowDark: Color(RAW_COLORS.green[900]) 30 | .alpha(0.5) 31 | .rgb() 32 | .string(), 33 | }; 34 | } 35 | 36 | case 'failed': { 37 | return { 38 | base: RAW_COLORS.red[500], 39 | highlight: RAW_COLORS.pink[300], 40 | pulseBase: RAW_COLORS.red[700], 41 | pulseHighlight: COLORS.lightError, 42 | shadowLight: Color(RAW_COLORS.red[900]) 43 | .alpha(0.25) 44 | .rgb() 45 | .string(), 46 | shadowDark: Color(RAW_COLORS.red[900]) 47 | .alpha(0.5) 48 | .rgb() 49 | .string(), 50 | }; 51 | } 52 | 53 | default: 54 | case 'idle': { 55 | return { 56 | base: RAW_COLORS.gray[700], 57 | highlight: RAW_COLORS.gray[500], 58 | pulseBase: RAW_COLORS.gray[700], 59 | pulseHighlight: RAW_COLORS.gray[500], 60 | shadowLight: Color(RAW_COLORS.gray[900]) 61 | .alpha(0.25) 62 | .rgb() 63 | .string(), 64 | shadowDark: Color(RAW_COLORS.gray[900]) 65 | .alpha(0.5) 66 | .rgb() 67 | .string(), 68 | }; 69 | } 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/components/LargeLED/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './LargeLED'; 3 | -------------------------------------------------------------------------------- /src/components/License/License.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, Fragment } from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { u1F4C3 as billIcon } from 'react-icons-kit/noto_emoji_regular/u1F4C3'; 6 | import { x as xIcon } from 'react-icons-kit/feather/x'; 7 | 8 | import { RAW_COLORS } from '../../constants'; 9 | 10 | import ExternalLink from '../ExternalLink'; 11 | import Spacer from '../Spacer'; 12 | 13 | type Props = { 14 | license: ?string, 15 | withIcon?: boolean, 16 | }; 17 | 18 | class License extends Component { 19 | render() { 20 | const { license, withIcon } = this.props; 21 | 22 | return ( 23 | 24 | {withIcon && ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | )} 32 | {license ? ( 33 | 34 | 35 | {license} 36 | 37 | license 38 | 39 | ) : ( 40 | 'No license found' 41 | )} 42 | 43 | ); 44 | } 45 | } 46 | 47 | const Wrapper = styled.span` 48 | display: inline-flex; 49 | align-items: center; 50 | `; 51 | 52 | const IconWrapper = styled.span` 53 | display: block; 54 | height: 24px; 55 | color: ${props => props.color}; 56 | `; 57 | 58 | const LicenseLink = styled(ExternalLink)` 59 | font-weight: 600; 60 | -webkit-font-smoothing: antialiased; 61 | `; 62 | 63 | export default License; 64 | -------------------------------------------------------------------------------- /src/components/License/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './License'; 3 | -------------------------------------------------------------------------------- /src/components/LoadingScreen/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './LoadingScreen'; 3 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | import guppyLogoSrc from '../../assets/images/guppy-logo.svg'; 5 | 6 | type Size = 'small' | 'medium' | 'large'; 7 | 8 | type Props = { 9 | size?: Size, 10 | grayscale?: boolean, 11 | }; 12 | 13 | class Logo extends Component { 14 | static defaultProps = { 15 | size: 'medium', 16 | }; 17 | 18 | render() { 19 | const { size, grayscale } = this.props; 20 | 21 | const desaturationAmount = grayscale ? 90 : 0; 22 | 23 | return ( 24 | Guppy logo 33 | ); 34 | } 35 | } 36 | 37 | const getLogoWidth = (size: ?Size) => { 38 | switch (size) { 39 | case 'small': 40 | return 24; 41 | case 'medium': 42 | return 48; 43 | case 'large': 44 | return 96; 45 | default: 46 | throw new Error('Unrecognized size for logo'); 47 | } 48 | }; 49 | 50 | export default Logo; 51 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import Showcase from '../../../.storybook/components/Showcase'; 7 | import Logo from './Logo'; 8 | 9 | storiesOf('Logo', module) 10 | .add( 11 | 'default', 12 | withInfo()(() => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | )) 31 | ) 32 | .add( 33 | 'unrecognized size', 34 | withInfo()(() => ( 35 | // $FlowFixMe - intentional error 36 | 37 | )) 38 | ); 39 | -------------------------------------------------------------------------------- /src/components/Logo/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Logo'; 3 | -------------------------------------------------------------------------------- /src/components/MainContentWrapper/MainContentWrapper.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | import { BREAKPOINTS } from '../../constants'; 5 | 6 | import { SIDEBAR_WIDTH } from '../Sidebar'; 7 | 8 | export default styled.div` 9 | width: 1050px; 10 | max-width: calc(100vw - ${SIDEBAR_WIDTH}px); 11 | 12 | @media ${BREAKPOINTS.sm} { 13 | padding: 40px; 14 | } 15 | 16 | @media ${BREAKPOINTS.mdMin} { 17 | padding: 75px; 18 | } 19 | `; 20 | -------------------------------------------------------------------------------- /src/components/MainContentWrapper/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './MainContentWrapper'; 3 | -------------------------------------------------------------------------------- /src/components/Middot/Middot.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | const Middot = () => ·; 5 | 6 | export default Middot; 7 | -------------------------------------------------------------------------------- /src/components/Middot/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Middot'; 3 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Modal'; 3 | -------------------------------------------------------------------------------- /src/components/ModalHeader/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ModalHeader'; 3 | -------------------------------------------------------------------------------- /src/components/Module/Module.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, Fragment } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import Heading from '../Heading'; 6 | import HelpButton from '../HelpButton'; 7 | 8 | type Props = { 9 | title: string, 10 | moreInfoHref: string, 11 | primaryActionChildren?: React$Node, 12 | children: React$Node, 13 | }; 14 | 15 | class Pane extends Component { 16 | render() { 17 | const { title, moreInfoHref, primaryActionChildren, children } = this.props; 18 | 19 | return ( 20 | 21 |
22 | 23 | {title} 24 | {moreInfoHref && } 25 | 26 | {primaryActionChildren} 27 |
28 | {children} 29 |
30 | ); 31 | } 32 | } 33 | 34 | const Header = styled.div` 35 | display: flex; 36 | justify-content: space-between; 37 | align-items: center; 38 | padding: 10px 0; 39 | `; 40 | 41 | const ActionWrapper = styled.div``; 42 | 43 | export default Pane; 44 | -------------------------------------------------------------------------------- /src/components/Module/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Module'; 3 | -------------------------------------------------------------------------------- /src/components/MountAfter/MountAfter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This is a hacky little component that helps us avoid rendering bugs and 4 | * other issues by offsetting the mount of a component by a certain amount of 5 | * time. 6 | */ 7 | import { Component } from 'react'; 8 | 9 | type Props = { 10 | delay: number, 11 | reason: string, 12 | children: React$Node, 13 | }; 14 | 15 | type State = { 16 | hasTimeElapsed: boolean, 17 | }; 18 | 19 | class MountAfter extends Component { 20 | state = { 21 | hasTimeElapsed: false, 22 | }; 23 | 24 | timeoutId: number; 25 | 26 | componentDidMount() { 27 | this.timeoutId = window.setTimeout(() => { 28 | this.setState({ hasTimeElapsed: true }); 29 | }, this.props.delay); 30 | } 31 | 32 | componentWillUnmount() { 33 | window.clearTimeout(this.timeoutId); 34 | } 35 | 36 | render() { 37 | const { children } = this.props; 38 | const { hasTimeElapsed } = this.state; 39 | 40 | return hasTimeElapsed ? children : null; 41 | } 42 | } 43 | 44 | export default MountAfter; 45 | -------------------------------------------------------------------------------- /src/components/MountAfter/MountAfter.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import Showcase from '../../../.storybook/components/Showcase'; 7 | import MountAfter from './MountAfter'; 8 | 9 | storiesOf('MountAfter', module).add( 10 | 'default', 11 | withInfo()(() => ( 12 | 13 | 14 | 15 | Hello World 16 | 17 | 18 | 19 | 20 | 21 | Hello World 22 | 23 | 24 | 25 | 26 | 27 | Hello World 28 | 29 | 30 | 31 | 32 | 33 | Hello World 34 | 35 | 36 | 37 | 38 | 39 | Hello World 40 | 41 | 42 | 43 | )) 44 | ); 45 | -------------------------------------------------------------------------------- /src/components/MountAfter/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './MountAfter'; 3 | -------------------------------------------------------------------------------- /src/components/NodeProvider/NodeProvider.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This app requires knowledge of where certain elements are on the page. 4 | * It should contain a map of critical HTML element nodes, and should provide 5 | * a consumer that can provide ref-capturers so that they can be gathered. 6 | * Also, resize handling presumably? 7 | */ 8 | import React, { Component } from 'react'; 9 | 10 | // $FlowFixMe 11 | const NodeContext = React.createContext('node'); 12 | 13 | export type Nodes = { [key: string]: HTMLElement }; 14 | export type BoundingBoxes = { [key: string]: ClientRect }; 15 | 16 | type Props = { children: React$Node }; 17 | type State = { 18 | nodes: Nodes, 19 | boundingBoxes: BoundingBoxes, 20 | }; 21 | 22 | class NodeProvider extends Component { 23 | state = { 24 | nodes: {}, 25 | boundingBoxes: {}, 26 | refCapturer: (id: string, node: HTMLElement) => { 27 | if (!node) { 28 | return; 29 | } 30 | 31 | if (this.state.nodes[id]) { 32 | return; 33 | } 34 | 35 | this.setState({ 36 | nodes: { 37 | ...this.state.nodes, 38 | [id]: node, 39 | }, 40 | boundingBoxes: { 41 | ...this.state.boundingBoxes, 42 | [id]: node.getBoundingClientRect(), 43 | }, 44 | }); 45 | }, 46 | }; 47 | 48 | render() { 49 | return ( 50 | 51 | {this.props.children} 52 | 53 | ); 54 | } 55 | } 56 | 57 | export const NodeConsumer = NodeContext.Consumer; 58 | 59 | export default NodeProvider; 60 | -------------------------------------------------------------------------------- /src/components/NodeProvider/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { NodeConsumer, default } from './NodeProvider'; 3 | -------------------------------------------------------------------------------- /src/components/OnlineChecker/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './OnlineChecker'; 3 | -------------------------------------------------------------------------------- /src/components/OnlyOn/OnlyOn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | import { BREAKPOINTS } from '../../constants'; 5 | 6 | type Size = $Keys; 7 | 8 | type Props = { 9 | size: Size, 10 | display: 'inline' | 'block' | 'inline-block', 11 | children: React$Node, 12 | }; 13 | 14 | class OnlyOn extends PureComponent { 15 | static defaultProps = { 16 | display: 'inline', 17 | }; 18 | 19 | getElement = (size: Size) => { 20 | switch (size) { 21 | case 'sm': 22 | return LessThanSmall; 23 | case 'md': 24 | return LessThanMedium; 25 | case 'mdMin': 26 | return MediumAndUp; 27 | case 'lgMin': 28 | return LargeAndUp; 29 | default: 30 | throw new Error('Unrecognized size to OnlyOn'); 31 | } 32 | }; 33 | render() { 34 | const { size, display, children, ...delegated } = this.props; 35 | 36 | const Element = this.getElement(size); 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | } 45 | 46 | const LessThanSmall = styled.span` 47 | display: none; 48 | @media ${BREAKPOINTS.sm} { 49 | display: ${props => props.display}; 50 | } 51 | `; 52 | 53 | const LessThanMedium = styled.span` 54 | display: none; 55 | @media ${BREAKPOINTS.md} { 56 | display: ${props => props.display}; 57 | } 58 | `; 59 | 60 | const LargeAndUp = styled.span` 61 | display: none; 62 | @media ${BREAKPOINTS.lgMin} { 63 | display: ${props => props.display}; 64 | } 65 | `; 66 | 67 | const MediumAndUp = styled.span` 68 | display: none; 69 | @media ${BREAKPOINTS.mdMin} { 70 | display: ${props => props.display}; 71 | } 72 | `; 73 | 74 | export default OnlyOn; 75 | -------------------------------------------------------------------------------- /src/components/OnlyOn/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './OnlyOn'; 3 | -------------------------------------------------------------------------------- /src/components/Paragraph/Paragraph.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | export default styled.div` 5 | font-size: 1.25rem; 6 | margin-bottom: 1.5rem; 7 | `; 8 | -------------------------------------------------------------------------------- /src/components/Paragraph/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Paragraph'; 3 | -------------------------------------------------------------------------------- /src/components/PixelShifter/PixelShifter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | 4 | type Props = { 5 | x?: number, 6 | y?: number, 7 | reason: string, 8 | style?: Object, 9 | children: React$Node, 10 | }; 11 | 12 | const PixelShifter = ({ 13 | x = 0, 14 | y = 0, 15 | reason, 16 | style = {}, 17 | children, 18 | }: Props) => ( 19 |
25 | {children} 26 |
27 | ); 28 | 29 | export default PixelShifter; 30 | -------------------------------------------------------------------------------- /src/components/PixelShifter/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './PixelShifter'; 3 | -------------------------------------------------------------------------------- /src/components/Planet/Orbit.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | type Props = { 6 | planetSize: number, 7 | duration: number, 8 | delay: number, 9 | children: React$Node, 10 | }; 11 | 12 | class PlanetCloud extends Component { 13 | static defaultProps = { 14 | color: '#FFF', 15 | duration: 50000, 16 | delay: 0, 17 | }; 18 | 19 | node: ?HTMLElement; 20 | 21 | componentDidMount() { 22 | const { planetSize, duration, delay } = this.props; 23 | const { node } = this; 24 | 25 | if (!node) { 26 | return; 27 | } 28 | 29 | const orbitAnimationFrames = [ 30 | { transform: `translateX(${planetSize * -1}px)` }, 31 | { transform: `translateX(${planetSize}px)` }, 32 | ]; 33 | 34 | const orbitAnimationTiming = { 35 | duration, 36 | delay, 37 | iterations: Infinity, 38 | }; 39 | 40 | // $FlowFixMe 41 | node.animate(orbitAnimationFrames, orbitAnimationTiming); 42 | } 43 | 44 | render() { 45 | const { duration, delay, planetSize, children, ...delegated } = this.props; 46 | 47 | return ( 48 | (this.node = node)} {...delegated}> 49 | {children} 50 | 51 | ); 52 | } 53 | } 54 | 55 | const Orbiter = styled.div` 56 | display: inline-block; 57 | `; 58 | 59 | export default PlanetCloud; 60 | -------------------------------------------------------------------------------- /src/components/Planet/Planet.helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const isFirstHalf = (index, totalNum) => { 3 | return index <= totalNum / 2; 4 | }; 5 | export const getCloudPathFromPoints = (points: Array) => { 6 | const [firstPoint, ...otherPoints] = points; 7 | 8 | const initialPosition = `M${firstPoint},0`; 9 | 10 | return otherPoints.reduce((acc, point, index) => { 11 | const numOfPoints = otherPoints.length; 12 | 13 | const isEven = index % 2 === 0; 14 | 15 | const rowNum = isFirstHalf(index, numOfPoints) 16 | ? index 17 | : numOfPoints - index; 18 | const nextRowNum = isFirstHalf(index + 1, numOfPoints) 19 | ? rowNum + 1 20 | : rowNum - 1; 21 | 22 | const largeArcFlag = isFirstHalf(index + 1, numOfPoints) 23 | ? isEven 24 | ? 1 25 | : 0 26 | : isEven 27 | ? 0 28 | : 1; 29 | 30 | const lineInstruction = `L${point},${rowNum}`; 31 | 32 | const arcInstruction = ` 33 | A 0.5 0.5 0 0 ${largeArcFlag} ${point} ${nextRowNum} 34 | `; 35 | 36 | return `${acc} ${lineInstruction} ${arcInstruction}`; 37 | }, initialPosition); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/Planet/Planet.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import styled from 'styled-components'; 5 | 6 | import Planet from './Planet'; 7 | import Earth from '../Earth'; 8 | 9 | storiesOf('Planet', module) 10 | .add('Basic', () => ( 11 | 12 | 16 | 17 | )) 18 | .add('Earth', () => ( 19 | 20 | 21 | 22 | )); 23 | 24 | const Space = styled.div` 25 | width: 500px; 26 | height: 500px; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | background: rgb(26, 17, 81); 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/Planet/PlanetGlow.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | 4 | type Props = { 5 | planetSize: number, 6 | topColor: string, 7 | bottomColor: string, 8 | }; 9 | 10 | class PlanetGlow extends Component { 11 | static defaultProps = { 12 | topColor: '#00b4ff', 13 | bottomColor: '#001e6a', 14 | }; 15 | render() { 16 | const { planetSize, topColor, bottomColor, ...delegated } = this.props; 17 | 18 | return ( 19 | 26 | 27 | 28 | 29 | 30 | 31 | 42 | 53 | 54 | ); 55 | } 56 | } 57 | 58 | export default PlanetGlow; 59 | -------------------------------------------------------------------------------- /src/components/Planet/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default as Planet } from './Planet'; 3 | export { default as PlanetCloud } from './PlanetCloud'; 4 | export { default as PlanetGlow } from './PlanetGlow'; 5 | export { default as PlanetMoon } from './PlanetMoon'; 6 | export { default as EarthContinents } from './EarthContinents'; 7 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | import { Spring, animated } from 'react-spring'; 5 | 6 | import { GRADIENTS } from '../../constants'; 7 | 8 | // TODO: consider renaming stiffness and damping to tension and friction 9 | type Props = { 10 | height: number, 11 | progress: number, 12 | stiffness: number, 13 | damping: number, 14 | colors: Array, 15 | reset: boolean, 16 | }; 17 | 18 | class ProgressBar extends Component { 19 | static defaultProps = { 20 | height: 8, 21 | stiffness: 32, 22 | damping: 32, 23 | colors: GRADIENTS.progress, 24 | reset: false, 25 | }; 26 | 27 | render() { 28 | const { height, progress, stiffness, damping, colors, reset } = this.props; 29 | 30 | return ( 31 | 32 | 39 | {interpolated => ( 40 | 44 | )} 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export const Wrapper = styled.div` 52 | position: relative; 53 | height: ${props => props.height}px; 54 | width: 100%; 55 | `; 56 | 57 | const ProgressGradient = animated(styled.div.attrs({ 58 | style: props => ({ 59 | clipPath: `polygon( 60 | 0% 0%, 61 | ${props.progress * 100}% 0%, 62 | ${props.progress * 100}% 100%, 63 | 0% 100% 64 | `, 65 | }), 66 | })` 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | right: 0; 71 | bottom: 0; 72 | background: linear-gradient(to right, ${props => props.colors.join(', ')}); 73 | `); 74 | 75 | export default ProgressBar; 76 | -------------------------------------------------------------------------------- /src/components/ProgressBar/ProgressBar.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import React from 'react'; 3 | import { shallow, mount } from 'enzyme'; 4 | import ProgressBar, { Wrapper } from './ProgressBar'; 5 | 6 | describe('ProgressBar component', () => { 7 | describe('should render', () => { 8 | // We could do the tests in a loop but for three test cases 9 | // it's OK to duplicate code 10 | it('with 0%', () => { 11 | const wrapper = shallow(); 12 | expect(wrapper).toMatchSnapshot(); 13 | }); 14 | 15 | it('with 50%', () => { 16 | const wrapper = shallow(); 17 | expect(wrapper).toMatchSnapshot(); 18 | }); 19 | 20 | it('with 100%', () => { 21 | const wrapper = shallow(); 22 | expect(wrapper).toMatchSnapshot(); 23 | }); 24 | }); 25 | 26 | it('should apply height style to Wrapper', () => { 27 | const wrapper = mount(); 28 | 29 | expect(wrapper.find(Wrapper)).toHaveStyleRule('height', '10px'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProgressBar'; 3 | -------------------------------------------------------------------------------- /src/components/ProjectConfigurationModal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProjectConfigurationModal'; 3 | -------------------------------------------------------------------------------- /src/components/ProjectIconSelection/ProjectIconSelection.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | import { action } from '@storybook/addon-actions'; 6 | 7 | import Showcase from '../../../.storybook/components/Showcase'; 8 | import ProjectIconSelection from './ProjectIconSelection'; 9 | 10 | storiesOf('ProjectIconSelection', module).add( 11 | 'default', 12 | withInfo()(() => ( 13 | 14 | 15 | 19 | 20 | 21 | 26 | 27 | 28 | 34 | 35 | 36 | 42 | 43 | 44 | )) 45 | ); 46 | -------------------------------------------------------------------------------- /src/components/ProjectIconSelection/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProjectIconSelection'; 3 | -------------------------------------------------------------------------------- /src/components/ProjectPage/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProjectPage'; 3 | -------------------------------------------------------------------------------- /src/components/ProjectTitle/ProjectTitle.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import ProjectTitle from './ProjectTitle'; 7 | import Showcase from '../../../.storybook/components/Showcase'; 8 | 9 | storiesOf('ProjectTitle', module).add( 10 | 'default', 11 | withInfo()(() => ( 12 | 13 | 14 | 19 | 20 | 21 | 22 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | )) 41 | ); 42 | -------------------------------------------------------------------------------- /src/components/ProjectTitle/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProjectTitle'; 3 | -------------------------------------------------------------------------------- /src/components/ProjectTypeSelection/ProjectTypeSelection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment, PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { GatsbyIcon, NextjsIcon, ReactIcon } from '../Icons'; 6 | import ButtonWithIcon from '../ButtonWithIcon'; 7 | import Spacer from '../Spacer'; 8 | 9 | import type { ProjectType } from '../../types'; 10 | type Props = { 11 | projectType: ?ProjectType, 12 | onProjectTypeSelect: (projectType: ProjectType) => void, 13 | }; 14 | 15 | class ProjectTypeSelection extends PureComponent { 16 | select = (ev: SyntheticEvent<*>, projectType: ProjectType) => { 17 | ev.preventDefault(); 18 | this.props.onProjectTypeSelect(projectType); 19 | }; 20 | 21 | render() { 22 | const { projectType } = this.props; 23 | return ( 24 | 25 | {mapProjectTypeToComponent.map((curProjectType, index) => ( 26 | 27 | ) => 31 | this.select(ev, curProjectType.type) 32 | } 33 | > 34 | {curProjectType.caption} 35 | 36 | 37 | 38 | ))} 39 | 40 | ); 41 | } 42 | } 43 | 44 | const ProjectTypeTogglesWrapper = styled.div` 45 | margin-top: 8px; 46 | margin-left: -8px; 47 | `; 48 | 49 | const mapProjectTypeToComponent = [ 50 | { 51 | type: 'create-react-app', 52 | Component: ReactIcon, 53 | caption: 'Vanilla React', 54 | }, 55 | { 56 | type: 'gatsby', 57 | Component: GatsbyIcon, 58 | caption: 'Gatsby', 59 | }, 60 | { 61 | type: 'nextjs', 62 | Component: NextjsIcon, 63 | caption: 'Next.js', 64 | }, 65 | ]; 66 | 67 | export default ProjectTypeSelection; 68 | -------------------------------------------------------------------------------- /src/components/ProjectTypeSelection/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ProjectTypeSelection'; 3 | -------------------------------------------------------------------------------- /src/components/ScrollDisabler/ScrollDisabler.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { PureComponent } from 'react'; 3 | 4 | class ScrollDisabler extends PureComponent<{}> { 5 | oldOverflow: string; 6 | oldPosition: string; 7 | oldWidth: string; 8 | oldHeight: string; 9 | oldTop: string; 10 | oldScrollY: string; 11 | 12 | componentDidMount() { 13 | if (document.body === null) { 14 | // that can't happen but let's make flow happy 15 | return; 16 | } 17 | this.oldOverflow = document.body.style.overflow; 18 | this.oldPosition = document.body.style.position; 19 | this.oldWidth = document.body.style.width; 20 | this.oldHeight = document.body.style.height; 21 | this.oldTop = document.body.style.top; 22 | 23 | this.oldScrollY = window.scrollY; 24 | 25 | document.body.style.overflow = 'hidden'; 26 | document.body.style.position = 'fixed'; 27 | document.body.style.width = '100%'; 28 | document.body.style.height = `calc(100% + ${this.oldScrollY}px)`; 29 | document.body.style.top = `-${this.oldScrollY}px`; 30 | } 31 | 32 | componentWillUnmount() { 33 | if (document.body === null) { 34 | // that can't happen but let's make flow happy 35 | return; 36 | } 37 | document.body.style.overflow = this.oldOverflow; 38 | document.body.style.position = this.oldPosition; 39 | document.body.style.width = this.oldWidth; 40 | document.body.style.height = this.oldHeight; 41 | document.body.style.top = this.oldTop; 42 | 43 | window.scrollTo(0, this.oldScrollY); 44 | } 45 | 46 | render() { 47 | return null; 48 | } 49 | } 50 | 51 | export default ScrollDisabler; 52 | -------------------------------------------------------------------------------- /src/components/ScrollDisabler/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './ScrollDisabler'; 3 | -------------------------------------------------------------------------------- /src/components/SelectableImage/SelectableImage.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import SelectableItem from '../SelectableItem'; 6 | import type { Props as SelectableItemProps, Status } from '../SelectableItem'; 7 | 8 | type Props = $Rest< 9 | SelectableItemProps, 10 | {| children: (status: Status) => React$Node |} 11 | > & { 12 | src: string, 13 | }; 14 | 15 | class SelectableImage extends PureComponent { 16 | render() { 17 | const { src, ...delegated } = this.props; 18 | 19 | return ( 20 | 21 | {status => } 22 | 23 | ); 24 | } 25 | } 26 | 27 | const Image = styled.img` 28 | position: relative; 29 | z-index: 1; 30 | width: 100%; 31 | height: 100%; 32 | border-radius: 50%; 33 | opacity: ${props => (props.status === 'faded' ? 0.55 : 1)}; 34 | transition: opacity 300ms; 35 | 36 | &:hover { 37 | opacity: 1; 38 | } 39 | `; 40 | 41 | export default SelectableImage; 42 | -------------------------------------------------------------------------------- /src/components/SelectableImage/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './SelectableImage'; 3 | -------------------------------------------------------------------------------- /src/components/SelectableItem/SelectableItem.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import CircularOutline from '../CircularOutline'; 6 | import DetectActive from '../DetectActive'; 7 | 8 | export type Status = 'default' | 'highlighted' | 'faded'; 9 | 10 | export type Props = { 11 | size: number, 12 | colors: Array, 13 | status: Status, 14 | children: (status: Status) => React$Node, 15 | }; 16 | 17 | class SelectableItem extends Component { 18 | render() { 19 | const { size, colors, status, children, ...delegated } = this.props; 20 | 21 | return ( 22 | 23 | {isActive => ( 24 | 25 | 26 | 32 | 33 | 34 | {children(status)} 35 | 36 | )} 37 | 38 | ); 39 | } 40 | } 41 | 42 | const ButtonElem = styled.button` 43 | position: relative; 44 | width: ${props => props.size}px; 45 | height: ${props => props.size}px; 46 | border: none; 47 | background: none; 48 | outline: none; 49 | padding: 0; 50 | cursor: pointer; 51 | 52 | &:active rect { 53 | stroke-width: 4; 54 | } 55 | `; 56 | 57 | const OutlineWrapper = styled.div` 58 | position: absolute; 59 | z-index: 3; 60 | top: -3px; 61 | left: -3px; 62 | right: -3px; 63 | bottom: -3px; 64 | pointer-events: none; 65 | `; 66 | 67 | export default SelectableItem; 68 | -------------------------------------------------------------------------------- /src/components/SelectableItem/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './SelectableItem'; 3 | export type { Status, Props } from './SelectableItem'; 4 | -------------------------------------------------------------------------------- /src/components/SettingsButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './SettingsButton'; 3 | -------------------------------------------------------------------------------- /src/components/Sidebar/AddProjectButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { plus } from 'react-icons-kit/feather/plus'; 6 | 7 | import { COLORS } from '../../constants'; 8 | 9 | type Props = { 10 | size: number, 11 | isVisible: boolean, 12 | onClick: () => void, 13 | isOnline: boolean, 14 | }; 15 | 16 | const AddProjectButton = ({ size, isVisible, onClick, isOnline }: Props) => ( 17 | 32 | ); 33 | 34 | const Button = styled.button` 35 | position: relative; 36 | width: ${props => props.size}px; 37 | height: ${props => props.size}px; 38 | outline: none; 39 | border: none; 40 | background: none; 41 | cursor: pointer; 42 | opacity: ${props => (props.isOnline ? 1 : 0.5)}; 43 | pointer-events: ${props => (props.isOnline ? 'auto' : 'none')}; 44 | `; 45 | 46 | const Background = styled.div` 47 | position: absolute; 48 | z-index: -1; 49 | top: 0; 50 | left: 0; 51 | right: 0; 52 | bottom: 0; 53 | background: ${COLORS.lightBackground}; 54 | opacity: 0.1; 55 | transition: opacity 300ms; 56 | border-radius: 100%; 57 | 58 | ${Button}:hover & { 59 | opacity: 0.2; 60 | } 61 | 62 | ${Button}:focus & { 63 | opacity: 0.3; 64 | } 65 | `; 66 | 67 | const IconWrapper = styled.div` 68 | transform: translate(1px, 2px); 69 | `; 70 | 71 | export default AddProjectButton; 72 | -------------------------------------------------------------------------------- /src/components/Sidebar/IntroductionBlurb.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { cornerUpLeft } from 'react-icons-kit/feather/cornerUpLeft'; 6 | 7 | import { RAW_COLORS } from '../../constants'; 8 | 9 | import Paragraph from '../Paragraph'; 10 | import Spacer from '../Spacer'; 11 | 12 | type Props = { 13 | isVisible: boolean, 14 | }; 15 | 16 | class IntroductionBlurb extends PureComponent { 17 | render() { 18 | const { isVisible } = this.props; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | This is the Projects Sidebar. 28 | 29 | 30 | 31 | Your new project was just added! As you create more projects, they'll 32 | show up here too. 33 | 34 | 35 | 36 | 37 | Click on your first project to select it. 38 | 39 | 40 | ); 41 | } 42 | } 43 | 44 | const Wrapper = styled.div.attrs({ 45 | style: props => ({ 46 | opacity: props.isVisible ? 1 : 0, 47 | transition: props.isVisible ? '2000ms 100ms' : '250ms', 48 | transform: props.isVisible ? 'translateX(100%)' : 'translateX(105%)', 49 | pointerEvents: props.isVisible ? 'auto' : 'none', 50 | }), 51 | })` 52 | position: absolute; 53 | top: 36px; 54 | right: 0; 55 | width: 450px; 56 | padding-left: 56px; 57 | color: ${RAW_COLORS.gray[800]}; 58 | will-change: transform; 59 | `; 60 | 61 | const LargeIconWrapper = styled.div` 62 | transform: translateX(-42px); 63 | `; 64 | 65 | const Heading = styled.h2` 66 | font-size: 32px; 67 | margin-bottom: 16px; 68 | `; 69 | 70 | const Em = styled.em` 71 | font-style: italic; 72 | color: ${RAW_COLORS.purple[500]}; 73 | `; 74 | 75 | export default IntroductionBlurb; 76 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarProjectIcon.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { COLORS } from '../../constants'; 6 | 7 | import SelectableImage from '../SelectableImage'; 8 | import SelectableItem from '../SelectableItem'; 9 | 10 | type Props = { 11 | id: string, 12 | size: number, 13 | name: string, 14 | color?: string, 15 | iconSrc?: string, 16 | isSelected: boolean, 17 | handleSelect: () => void, 18 | }; 19 | 20 | const SidebarProjectIcon = ({ 21 | id, 22 | size, 23 | name, 24 | color, 25 | iconSrc, 26 | isSelected, 27 | handleSelect, 28 | }: Props) => { 29 | const sharedProps = { 30 | size, 31 | colors: [COLORS.lightBackground], 32 | status: isSelected ? 'highlighted' : 'faded', 33 | onClick: handleSelect, 34 | }; 35 | 36 | // For projects with an icon, we want to render a selectable image, with 37 | // that icon. For imported projects with no icon, we instead want to render 38 | // a circle with the first letter of that project name. 39 | return ( 40 | 41 | {iconSrc ? ( 42 | 43 | ) : ( 44 | 45 | {status => ( 46 | 47 | {name.slice(0, 1).toUpperCase()} 48 | 49 | )} 50 | 51 | )} 52 | 53 | ); 54 | }; 55 | 56 | const Wrapper = styled.div` 57 | img { 58 | pointer-events: none; 59 | } 60 | `; 61 | 62 | export const ProjectNameIcon = styled.div` 63 | position: relative; 64 | width: 100%; 65 | height: 100%; 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | font-size: 24px; 70 | color: ${COLORS.textOnBackground}; 71 | border-radius: 50%; 72 | `; 73 | 74 | export default SidebarProjectIcon; 75 | -------------------------------------------------------------------------------- /src/components/Sidebar/SidebarProjectIcon.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | 4 | import SidebarProjectIcon, { ProjectNameIcon } from './SidebarProjectIcon'; 5 | import SelectableImage from './../SelectableImage/SelectableImage'; 6 | import SelectableItem from '../SelectableItem'; 7 | 8 | describe('SidebarProjectIcon component', () => { 9 | let wrapper; 10 | beforeEach(() => { 11 | wrapper = mount(); 12 | }); 13 | 14 | it('should render projectIcon with first letter', () => { 15 | expect(wrapper.find(ProjectNameIcon).text()).toEqual('M'); 16 | }); 17 | 18 | it('should render projectIcon as SeletableImage', () => { 19 | wrapper.setProps({ iconSrc: 'icon' }); 20 | expect(wrapper.find(SelectableImage).exists()).toBe(true); 21 | }); 22 | 23 | it('should set faded status if not selected', () => { 24 | expect(wrapper.find(SelectableItem).prop('status')).toEqual('faded'); 25 | }); 26 | 27 | it('should set highlighted status if selected', () => { 28 | wrapper.setProps({ 29 | isSelected: true, 30 | }); 31 | expect(wrapper.find(SelectableItem).prop('status')).toEqual('highlighted'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default, SIDEBAR_WIDTH } from './Sidebar'; 3 | -------------------------------------------------------------------------------- /src/components/Spacer/Spacer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | export default styled.div` 5 | display: ${props => (props.inline ? 'inline-block' : 'block')}; 6 | width: ${props => props.size}px; 7 | height: ${props => props.size}px; 8 | `; 9 | -------------------------------------------------------------------------------- /src/components/Spacer/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Spacer'; 3 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled, { keyframes } from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { loader } from 'react-icons-kit/feather/loader'; 6 | 7 | import { RAW_COLORS } from '../../constants'; 8 | 9 | type Props = { 10 | size: number, 11 | color?: string, 12 | }; 13 | 14 | const Spinner = ({ size, color = RAW_COLORS.gray[500] }: Props) => ( 15 | 16 | ); 17 | 18 | const spin = keyframes` 19 | from { 20 | transform: rotate(0deg); 21 | } 22 | 23 | to { 24 | transform: rotate(360deg); 25 | } 26 | `; 27 | 28 | const Icon = styled(IconBase)` 29 | animation: ${spin} 2s linear infinite; 30 | color: ${props => props.color}; 31 | `; 32 | 33 | export default Spinner; 34 | -------------------------------------------------------------------------------- /src/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Spinner'; 3 | -------------------------------------------------------------------------------- /src/components/TaskDetailsModal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TaskDetailsModal'; 3 | -------------------------------------------------------------------------------- /src/components/TaskRunnerPane/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TaskRunnerPane'; 3 | -------------------------------------------------------------------------------- /src/components/TaskRunnerPaneRow/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TaskRunnerPaneRow'; 3 | -------------------------------------------------------------------------------- /src/components/TerminalOutput/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TerminalOutput'; 3 | -------------------------------------------------------------------------------- /src/components/TextButton/TextButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import styled from 'styled-components'; 3 | 4 | const TextButton = styled.button` 5 | background: transparent; 6 | border: none; 7 | outline: none; 8 | text-decoration: underline; 9 | padding: 0; 10 | margin: 0; 11 | color: inherit; 12 | font-size: inherit; 13 | cursor: pointer; 14 | `; 15 | 16 | export default TextButton; 17 | -------------------------------------------------------------------------------- /src/components/TextButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TextButton'; 3 | -------------------------------------------------------------------------------- /src/components/TextInput/TextInput.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | import { RAW_COLORS, COLORS } from '../../constants'; 6 | 7 | type Props = { 8 | isFocused?: boolean, 9 | hasError?: boolean, 10 | children?: React$Node, 11 | }; 12 | 13 | const TextInput = ({ isFocused, hasError, children, ...delegated }: Props) => ( 14 | 15 | 16 | {children} 17 | 18 | ); 19 | 20 | const getBorderColor = (props: Props) => { 21 | if (props.hasError) { 22 | return COLORS.error; 23 | } else if (props.isFocused) { 24 | return RAW_COLORS.purple[700]; 25 | } else { 26 | return RAW_COLORS.gray[700]; 27 | } 28 | }; 29 | 30 | const Wrapper = styled.div` 31 | width: 100%; 32 | 33 | display: flex; 34 | border-bottom: 2px solid ${getBorderColor}; 35 | `; 36 | 37 | const InputElem = styled.input` 38 | flex: 1; 39 | padding: 8px 0px; 40 | border: none; 41 | border-radius: 0px; 42 | outline: none; 43 | font-size: 21px; 44 | 45 | &::placeholder { 46 | color: ${RAW_COLORS.gray[300]}; 47 | } 48 | `; 49 | 50 | export default TextInput; 51 | -------------------------------------------------------------------------------- /src/components/TextInput/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TextInput'; 3 | -------------------------------------------------------------------------------- /src/components/TextInputWithButton/TextInputWithButton.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { PureComponent } from 'react'; 3 | import styled from 'styled-components'; 4 | import IconBase from 'react-icons-kit'; 5 | import { moreHorizontal } from 'react-icons-kit/feather/moreHorizontal'; 6 | 7 | import { RAW_COLORS } from '../../constants'; 8 | 9 | import TextInput from '../TextInput'; 10 | import HoverableOutlineButton from '../HoverableOutlineButton'; 11 | 12 | type Props = { 13 | value: string, 14 | handleFocus: string => void, 15 | onChange: string => void, 16 | onClick: () => void, 17 | onFocus: string => void, 18 | isFocused?: boolean, 19 | icon: React$Node, 20 | }; 21 | 22 | class TextInputWithButton extends PureComponent { 23 | static defaultProps = { 24 | value: '', 25 | onFocus: () => {}, 26 | icon: moreHorizontal, 27 | }; 28 | 29 | render() { 30 | const { onChange, onClick, icon, ...delegated } = this.props; 31 | 32 | return ( 33 | 34 | onChange(ev.target.value)}> 35 | 38 | window.requestAnimationFrame(delegated.handleFocus) 39 | } 40 | onClick={onClick} 41 | style={{ width: 32, height: 32 }} 42 | > 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | const Wrapper = styled.div` 54 | color: ${RAW_COLORS.gray[400]}; 55 | `; 56 | 57 | const ButtonPositionAdjuster = styled.div` 58 | transform: translateY(2px); 59 | `; 60 | 61 | export default TextInputWithButton; 62 | -------------------------------------------------------------------------------- /src/components/TextInputWithButton/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TextInputWithButton'; 3 | -------------------------------------------------------------------------------- /src/components/Titlebar/Titlebar.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * The title bar in this app is invisible, and yet it still needs to be defined, 4 | * as it constitutes the "draggable" area of the window. 5 | */ 6 | import styled from 'styled-components'; 7 | 8 | import { Z_INDICES } from '../../constants'; 9 | 10 | export default styled.div` 11 | position: fixed; 12 | z-index: ${Z_INDICES.titlebar}; 13 | top: 0; 14 | left: 0; 15 | right: 0; 16 | height: 30px; 17 | -webkit-app-region: drag; 18 | -webkit-user-select: none; 19 | `; 20 | -------------------------------------------------------------------------------- /src/components/Titlebar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Titlebar'; 3 | -------------------------------------------------------------------------------- /src/components/Toggle/Toggle.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component, Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import Showcase from '../../../.storybook/components/Showcase'; 7 | import Toggle from './Toggle'; 8 | 9 | class StatefulToggle extends Component { 10 | state = { 11 | isToggled: true, 12 | }; 13 | 14 | toggle = () => { 15 | this.setState(state => ({ isToggled: !state.isToggled })); 16 | }; 17 | 18 | render() { 19 | return ( 20 | 25 | ); 26 | } 27 | } 28 | 29 | storiesOf('Toggle', module).add( 30 | 'default', 31 | withInfo()(() => ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | )) 54 | ); 55 | -------------------------------------------------------------------------------- /src/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './Toggle'; 3 | -------------------------------------------------------------------------------- /src/components/TwoPaneModal/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './TwoPaneModal'; 3 | -------------------------------------------------------------------------------- /src/components/WhimsicalInstaller/WhimsicalInstaller.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import styled from 'styled-components'; 5 | 6 | import { RAW_COLORS } from '../../constants'; 7 | 8 | import WhimsicalInstaller from './WhimsicalInstaller'; 9 | 10 | storiesOf('WhimsicalInstaller', module) 11 | .add('default (400px)', () => ( 12 | 13 | 14 | 15 | )) 16 | .add('Tiny (200px)', () => ( 17 | 18 | 19 | 20 | )) 21 | .add('Large (600px)', () => ( 22 | 23 | 24 | 25 | )) 26 | .add('Not running', () => ( 27 | 28 | 29 | 30 | )); 31 | 32 | const Wrapper = styled.div` 33 | width: ${props => props.width}px; 34 | height: ${props => props.height}px; 35 | background: ${RAW_COLORS.blue[700]}; 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/WhimsicalInstaller/WhimsicalInstaller.types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export type Point = { x: number, y: number }; 3 | 4 | export type BezierPath = { 5 | startPoint: Point, 6 | endPoint: Point, 7 | controlPoint: Point, 8 | }; 9 | 10 | export type FileStatus = 11 | | 'autonomous' // Flying autonomously towards the file 12 | | 'being-captured' // Very close to the folder, being sucked in 13 | | 'captured' // At the very center of the folder, no longer active 14 | | 'caught' // The user is grabbing the file 15 | | 'released'; // The user has released a previously-grabbed file 16 | 17 | export type FileData = { 18 | id: string, 19 | x: number, 20 | y: number, 21 | status: FileStatus, 22 | size: number, 23 | flightPath: BezierPath, 24 | speed?: { 25 | horizontalSpeed: number, 26 | verticalSpeed: number, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/WhimsicalInstaller/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './WhimsicalInstaller'; 3 | -------------------------------------------------------------------------------- /src/components/WindowDimensions/WindowDimensions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Component } from 'react'; 3 | 4 | import { throttle } from '../../utils'; 5 | 6 | type State = { 7 | width: number, 8 | height: number, 9 | }; 10 | type Props = { 11 | children: (dimensions: State) => React$Node, 12 | }; 13 | 14 | class WindowDimensions extends Component { 15 | state = { 16 | width: window.innerWidth, 17 | height: window.innerHeight, 18 | }; 19 | 20 | componentDidMount() { 21 | window.addEventListener('resize', this.handleResize); 22 | } 23 | 24 | componentWillUnmount() { 25 | window.removeEventListener('resize', this.handleResize); 26 | } 27 | 28 | handleResize = throttle(() => { 29 | this.setState({ 30 | width: window.innerWidth, 31 | height: window.innerHeight, 32 | }); 33 | }, 500); 34 | 35 | render() { 36 | return this.props.children(this.state); 37 | } 38 | } 39 | 40 | export default WindowDimensions; 41 | -------------------------------------------------------------------------------- /src/components/WindowDimensions/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export { default } from './WindowDimensions'; 3 | -------------------------------------------------------------------------------- /src/config/app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // app-wide settings (no user changable settings here) 3 | module.exports = { 4 | IN_APP_FEEDBACK_URL: 'https://guppy.nolt.io', 5 | PACKAGE_MANAGER: 'yarn', 6 | // Enable logging, if enabled all terminal responses are visible in the console (useful for debugging) 7 | LOGGING: false, 8 | }; 9 | -------------------------------------------------------------------------------- /src/config/project-types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | // project type configuration 4 | // used for 5 | // - create project command args 6 | // - devServer name mapping 7 | // 8 | import type { ProjectType } from '../types'; 9 | 10 | const config: { 11 | [projectType: ProjectType]: { 12 | devServer: { 13 | taskName: string, 14 | args: Array, 15 | env?: { 16 | [envVariable: string]: string, 17 | }, 18 | }, 19 | create: { 20 | args: Array, 21 | }, 22 | }, 23 | } = { 24 | 'create-react-app': { 25 | devServer: { 26 | taskName: 'start', 27 | args: ['run', 'start'], 28 | env: { 29 | PORT: '$port', 30 | }, 31 | }, 32 | create: { 33 | // not sure if we need that nesting but I think there could be more to configure 34 | args: [ 35 | // used for project creation previous getBuildInstructions 36 | 'create-react-app', 37 | '$projectPath', 38 | ], 39 | }, 40 | }, 41 | gatsby: { 42 | devServer: { 43 | taskName: 'develop', 44 | // gatsby needs -p instead of env for port changing 45 | args: ['run', 'develop', '-p', '$port'], 46 | }, 47 | create: { 48 | // not sure if we need that nesting but I think there could be more to configure 49 | args: [ 50 | // used for project creation previous getBuildInstructions 51 | 'gatsby', 52 | 'new', 53 | '$projectPath', 54 | '$projectStarter', 55 | ], 56 | }, 57 | }, 58 | nextjs: { 59 | devServer: { 60 | taskName: 'dev', 61 | args: ['run', 'dev', '-p', '$port'], 62 | }, 63 | create: { 64 | args: [ 65 | 'github:awolf81/create-next-app', // later will be 'create-next-app' --> added a comment to the following issue https://github.com/segmentio/create-next-app/issues/30 66 | '$projectPath', 67 | ], 68 | }, 69 | }, 70 | }; 71 | 72 | export default config; 73 | -------------------------------------------------------------------------------- /src/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Futura PT'; 3 | src: url('./assets/fonts/futura/FuturaPT-Bold.woff') format('woff'); 4 | font-weight: 900; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Futura PT'; 10 | src: url('./assets/fonts/futura/FuturaPT-Demi.woff') format('woff'); 11 | font-weight: 700; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Futura PT'; 17 | src: url('./assets/fonts/futura/FuturaPT-Medium.woff') format('woff'); 18 | font-weight: 500; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: 'Futura PT'; 24 | src: url('./assets/fonts/futura/FuturaPT-Book.woff') format('woff'); 25 | font-weight: 400; 26 | font-style: normal; 27 | } 28 | 29 | @font-face { 30 | font-family: 'Futura PT'; 31 | src: url('./assets/fonts/futura/FuturaPT-Light.woff') format('woff'); 32 | font-weight: 300; 33 | font-style: normal; 34 | } 35 | 36 | @font-face { 37 | font-family: 'Fira Mono'; 38 | src: url('./assets/fonts/fira-mono/FiraMono-Bold.ttf') format('woff'); 39 | font-weight: 700; 40 | font-style: normal; 41 | } 42 | 43 | @font-face { 44 | font-family: 'Fira Mono'; 45 | src: url('./assets/fonts/fira-mono/FiraMono-Medium.ttf') format('woff'); 46 | font-weight: 600; 47 | font-style: normal; 48 | } 49 | 50 | @font-face { 51 | font-family: 'Fira Mono'; 52 | src: url('./assets/fonts/fira-mono/FiraMono-Regular.ttf') format('woff'); 53 | font-weight: 400; 54 | font-style: normal; 55 | } 56 | -------------------------------------------------------------------------------- /src/global-styles.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { injectGlobal } from 'styled-components'; 3 | import 'react-tippy/dist/tippy.css'; 4 | import { COLORS } from './constants'; 5 | import './fonts.css'; 6 | import './base.css'; 7 | 8 | injectGlobal` 9 | html, 10 | body, 11 | input, 12 | button, 13 | select, 14 | option { 15 | /* This is important for MacOS Mojave's dark mode */ 16 | color: ${COLORS.text}; 17 | } 18 | 19 | body { 20 | background: ${COLORS.background}; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import configureStore from './store'; 7 | 8 | import App from './components/App'; 9 | import NodeProvider from './components/NodeProvider'; 10 | import DevTools from './components/DevTools'; 11 | 12 | import './global-styles'; 13 | 14 | const store = configureStore(); 15 | 16 | const root = document.getElementById('root'); 17 | 18 | if (!root) { 19 | throw new Error('Missing root container'); 20 | } 21 | 22 | ReactDOM.render( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | , 31 | root 32 | ); 33 | -------------------------------------------------------------------------------- /src/reducers/__snapshots__/projects.reducer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Selectors should getProjectById 1`] = ` 4 | Object { 5 | "color": "black", 6 | "createdAt": 12345, 7 | "dependencies": Array [ 8 | Object { 9 | "first-dep": Object {}, 10 | "second-dep": Object {}, 11 | }, 12 | Object { 13 | "first-dep": Object {}, 14 | "second-dep": Object {}, 15 | }, 16 | ], 17 | "icon": "http://example.com/link/to/pic", 18 | "id": "test-id", 19 | "name": "test", 20 | "path": "project-path/", 21 | "tasks": Array [], 22 | "type": "create-react-app", 23 | } 24 | `; 25 | 26 | exports[`Selectors should getProjectsArray 1`] = ` 27 | Array [ 28 | Object { 29 | "color": "black", 30 | "createdAt": 12345, 31 | "dependencies": Array [ 32 | Object {}, 33 | Object {}, 34 | ], 35 | "icon": "http://example.com/link/to/pic", 36 | "id": "foo", 37 | "name": "Foo", 38 | "path": undefined, 39 | "tasks": Array [], 40 | "type": "create-react-app", 41 | }, 42 | Object { 43 | "color": "black", 44 | "createdAt": 12345, 45 | "dependencies": Array [ 46 | Object {}, 47 | Object {}, 48 | ], 49 | "icon": "http://example.com/link/to/pic", 50 | "id": "bar", 51 | "name": "Bar", 52 | "path": undefined, 53 | "tasks": Array [], 54 | "type": "create-react-app", 55 | }, 56 | ] 57 | `; 58 | -------------------------------------------------------------------------------- /src/reducers/app-loaded.reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * There is a certain amount of initialization that needs to happen before 4 | * it makes sense to show anything to the user. 5 | * 6 | * Specifically: 7 | * 8 | * - We need to load persisted redux state, to know if the user is onboarding, 9 | * what their projects are, etc. 10 | * - We want to parse the projects on disk to get up-to-date info, because 11 | * the persisted redux state is incomplete; doesn't have info about tasks. 12 | * 13 | * This simple boolean reducer defaults to `false` and is toggled to `true` 14 | * once enough state has been loaded for us to show the user some UI. 15 | */ 16 | 17 | import { REFRESH_PROJECTS_FINISH } from '../actions'; 18 | 19 | import type { Action } from '../actions/types'; 20 | 21 | type State = boolean; 22 | 23 | const initialState = false; 24 | 25 | export default (state: State = initialState, action: Action = {}) => { 26 | switch (action.type) { 27 | case REFRESH_PROJECTS_FINISH: 28 | return true; 29 | 30 | default: 31 | return state; 32 | } 33 | }; 34 | 35 | // 36 | // 37 | // 38 | // Selectors 39 | export const getAppLoaded = (state: any) => state.appLoaded; 40 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from 'redux'; 3 | 4 | import appSettings from './app-settings.reducer'; 5 | import appLoaded from './app-loaded.reducer'; 6 | import appStatus from './app-status.reducer'; 7 | import projects from './projects.reducer'; 8 | import tasks from './tasks.reducer'; 9 | import dependencies from './dependencies.reducer'; 10 | import modal from './modal.reducer'; 11 | import onboardingStatus from './onboarding-status.reducer'; 12 | import paths from './paths.reducer'; 13 | import queue from './queue.reducer'; 14 | 15 | export default combineReducers({ 16 | appSettings, 17 | appLoaded, 18 | appStatus, 19 | projects, 20 | tasks, 21 | dependencies, 22 | modal, 23 | onboardingStatus, 24 | paths, 25 | queue, 26 | }); 27 | -------------------------------------------------------------------------------- /src/reducers/modal.reducer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * NOTE: Not all modals use this reducer. 4 | * Clicking a task details button just uses local state. 5 | * Unclear to me if it makes sense to keep this in Redux or not. 6 | */ 7 | import { 8 | CREATE_NEW_PROJECT_START, 9 | CREATE_NEW_PROJECT_CANCEL, 10 | CREATE_NEW_PROJECT_FINISH, 11 | IMPORT_EXISTING_PROJECT_START, 12 | SAVE_PROJECT_SETTINGS_FINISH, 13 | SHOW_PROJECT_SETTINGS, 14 | SHOW_APP_SETTINGS, 15 | HIDE_MODAL, 16 | RESET_ALL_STATE, 17 | } from '../actions'; 18 | 19 | import type { Action } from '../actions/types'; 20 | 21 | type State = 22 | | 'new-project-wizard' 23 | | 'project-settings' 24 | | 'new-project-wizard/select-starter' 25 | | null; 26 | 27 | export const initialState = null; 28 | 29 | export default (state: State = initialState, action: Action = {}) => { 30 | switch (action.type) { 31 | case CREATE_NEW_PROJECT_START: 32 | return 'new-project-wizard'; 33 | 34 | case SHOW_PROJECT_SETTINGS: 35 | return 'project-settings'; 36 | 37 | case SHOW_APP_SETTINGS: 38 | return 'app-settings'; 39 | 40 | case CREATE_NEW_PROJECT_CANCEL: 41 | case CREATE_NEW_PROJECT_FINISH: 42 | case IMPORT_EXISTING_PROJECT_START: 43 | case SAVE_PROJECT_SETTINGS_FINISH: 44 | case HIDE_MODAL: 45 | return null; 46 | 47 | case RESET_ALL_STATE: 48 | return initialState; 49 | 50 | default: 51 | return state; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/reducers/modal.reducer.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import reducer, { initialState } from './modal.reducer'; 3 | import { 4 | CREATE_NEW_PROJECT_START, 5 | CREATE_NEW_PROJECT_CANCEL, 6 | CREATE_NEW_PROJECT_FINISH, 7 | IMPORT_EXISTING_PROJECT_START, 8 | SAVE_PROJECT_SETTINGS_FINISH, 9 | SHOW_PROJECT_SETTINGS, 10 | SHOW_APP_SETTINGS, 11 | HIDE_MODAL, 12 | RESET_ALL_STATE, 13 | } from '../actions'; 14 | 15 | describe('Modal reducer', () => { 16 | it('should return initial state', () => { 17 | expect(reducer()).toEqual(initialState); 18 | }); 19 | 20 | const modalShowActions = [ 21 | { actionType: CREATE_NEW_PROJECT_START, expected: 'new-project-wizard' }, 22 | { actionType: SHOW_PROJECT_SETTINGS, expected: 'project-settings' }, 23 | { actionType: SHOW_APP_SETTINGS, expected: 'app-settings' }, 24 | ]; 25 | 26 | const modalCloseActions = [ 27 | CREATE_NEW_PROJECT_CANCEL, 28 | CREATE_NEW_PROJECT_FINISH, 29 | IMPORT_EXISTING_PROJECT_START, 30 | SAVE_PROJECT_SETTINGS_FINISH, 31 | HIDE_MODAL, 32 | ]; 33 | 34 | modalShowActions.forEach(action => { 35 | it(`should handle ${action.actionType}`, () => { 36 | const newState = reducer(initialState, { type: action.actionType }); 37 | expect(newState).toEqual(action.expected); 38 | }); 39 | }); 40 | 41 | modalCloseActions.forEach(action => { 42 | const prevState = 'new-project-wizard'; 43 | 44 | it(`should close modal with action ${action}`, () => { 45 | expect(reducer(prevState, { type: action })).toEqual(null); 46 | }); 47 | }); 48 | 49 | it(`should reset state on ${RESET_ALL_STATE}`, () => { 50 | const prevState = 'new-project-wizard'; 51 | expect(reducer(prevState, { type: RESET_ALL_STATE })).toEqual(null); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/sagas/development.saga.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { remote } from 'electron'; 3 | import { call, put, takeEvery } from 'redux-saga/effects'; 4 | 5 | import { SHOW_RESET_STATE_PROMPT, resetAllState } from '../actions'; 6 | import electronStore from '../services/electron-store.service'; 7 | 8 | import type { Saga } from 'redux-saga'; 9 | 10 | const { dialog } = remote; 11 | 12 | export function* handleShowResetDialog(): Saga { 13 | const response = yield call([dialog, dialog.showMessageBox], { 14 | type: 'warning', 15 | buttons: ['Reset', 'Cancel'], 16 | defaultId: 1, 17 | cancelId: 1, 18 | title: `Reset state`, 19 | message: `Are you sure you want to reset your application state?`, 20 | detail: `All your imported & created projects are removed from the application.\nThey're still on your disk but you need to re-import them. Only use this if you're having problems with a broken application state`, 21 | }); 22 | 23 | const confirmed = response === 0; 24 | if (confirmed) { 25 | yield call([electronStore, electronStore.clear]); 26 | yield put(resetAllState()); 27 | } 28 | } 29 | 30 | export default function* rootSaga(): Saga { 31 | yield takeEvery(SHOW_RESET_STATE_PROMPT, handleShowResetDialog); 32 | } 33 | -------------------------------------------------------------------------------- /src/sagas/development.saga.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import { call, put, takeEvery } from 'redux-saga/effects'; 3 | 4 | import { SHOW_RESET_STATE_PROMPT, resetAllState } from '../actions'; 5 | import electronStore from '../services/electron-store.service'; 6 | import rootSaga, { handleShowResetDialog } from './development.saga'; 7 | 8 | describe('development saga', () => { 9 | describe('root development saga', () => { 10 | it('should watch for start actions', () => { 11 | const saga = rootSaga(); 12 | expect(saga.next().value).toEqual( 13 | takeEvery(SHOW_RESET_STATE_PROMPT, handleShowResetDialog) 14 | ); 15 | }); 16 | }); 17 | 18 | describe('Reset state', () => { 19 | it('should reset state to initialState', () => { 20 | const saga = handleShowResetDialog(); 21 | 22 | // Show dialog 23 | saga.next(); 24 | 25 | // Confirm & check that electronStore.clear is called 26 | expect(saga.next(0).value).toEqual( 27 | call([electronStore, electronStore.clear]) 28 | ); 29 | 30 | // Trigger resetAllState action 31 | expect(saga.next().value).toEqual(put(resetAllState())); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { all } from 'redux-saga/effects'; 3 | 4 | import refreshProjectsSaga from './refresh-projects.saga'; 5 | import saveProjectSettingsSaga from './save-project-settings.saga'; 6 | import deleteProjectSaga from './delete-project.saga'; 7 | import dependencySaga from './dependency.saga'; 8 | import importProjectSaga from './import-project.saga'; 9 | import taskSaga from './task.saga'; 10 | import developmentSaga from './development.saga'; 11 | import queueSaga from './queue.saga'; 12 | import analyticsSaga from './analytics.saga'; 13 | 14 | // $FlowFixMe 15 | export default function*() { 16 | yield all([ 17 | // $FlowFixMe 18 | refreshProjectsSaga(), 19 | // $FlowFixMe 20 | deleteProjectSaga(), 21 | // $FlowFixMe 22 | dependencySaga(), 23 | // $FlowFixMe 24 | importProjectSaga(), 25 | // $FlowFixMe 26 | taskSaga(), 27 | // $FlowFixMe 28 | saveProjectSettingsSaga(), 29 | // $FlowFixMe 30 | developmentSaga(), 31 | // $FlowFixMe 32 | queueSaga(), 33 | // $FlowFixMe 34 | analyticsSaga(), 35 | ]); 36 | } 37 | -------------------------------------------------------------------------------- /src/sagas/refresh-projects.saga.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { call, put, select, takeEvery } from 'redux-saga/effects'; 3 | 4 | import { 5 | refreshProjectsFinish, 6 | refreshProjectsError, 7 | REFRESH_PROJECTS_START, 8 | } from '../actions'; 9 | import { loadGuppyProjects } from '../services/read-from-disk.service'; 10 | import { getPathsArray } from '../reducers/paths.reducer'; 11 | import type { Saga } from 'redux-saga'; 12 | 13 | export function* refreshProjects(): Saga { 14 | const pathsArray = yield select(getPathsArray); 15 | 16 | try { 17 | const projectsFromDisk = yield call(loadGuppyProjects, pathsArray); 18 | 19 | yield put(refreshProjectsFinish(projectsFromDisk)); 20 | } catch (err) { 21 | yield put(refreshProjectsError(err)); 22 | } 23 | } 24 | 25 | export default function* rootSaga(): Saga { 26 | yield takeEvery(REFRESH_PROJECTS_START, refreshProjects); 27 | } 28 | -------------------------------------------------------------------------------- /src/services/analytics.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * We currently use Mixpanel for our analytics. 4 | * 5 | * The goal of this file is to abstract that API, so that it can easily be 6 | * swapped in the future for a different provider. 7 | */ 8 | 9 | import mixpanel from 'mixpanel-browser'; 10 | import uuid from 'uuid/v1'; 11 | 12 | import electronStore from './electron-store.service'; 13 | 14 | export const MIXPANEL_KEY = '5840a1a4b9bdcdb518471f0e0830baa2'; 15 | const DISTINCT_ID_KEY = 'distinct-id'; 16 | 17 | export type EventType = 18 | | 'load-application' 19 | | 'create-project' 20 | | 'import-project' 21 | | 'select-project' 22 | | 'launch-dev-server' 23 | | 'run-task' 24 | | 'clear-console' 25 | | 'add-dependency' 26 | | 'update-dependency' 27 | | 'delete-dependency' 28 | | 'delete-project'; 29 | 30 | export const createLogger = (environment?: ?string = process.env.NODE_ENV) => { 31 | mixpanel.init(MIXPANEL_KEY); 32 | 33 | // Every user is given a distinct ID so that we can track return visits. 34 | // Because electron doesn't persist cookies, we have to do this ourselves. 35 | let distinctId = electronStore.get(DISTINCT_ID_KEY); 36 | if (!distinctId) { 37 | distinctId = uuid(); 38 | electronStore.set(DISTINCT_ID_KEY, distinctId); 39 | } 40 | 41 | mixpanel.identify(distinctId); 42 | 43 | return { 44 | logEvent: (event: EventType, data: any) => { 45 | if (environment !== 'production') { 46 | console.info('Event tracked', event, data); 47 | return; 48 | } 49 | 50 | if (mixpanel.has_opted_out_tracking()) { 51 | return; 52 | } 53 | 54 | mixpanel.track(event, data); 55 | }, 56 | }; 57 | }; 58 | 59 | // Export a singleton so that multiple modules can use the same instance. 60 | // We'll only ever want to create alternatives in tests. 61 | export default createLogger(); 62 | -------------------------------------------------------------------------------- /src/services/check-if-url-exists.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fetch from 'node-fetch'; 3 | 4 | export const urlExists = (url: string) => 5 | new Promise(async resolve => { 6 | const response = await fetch(url); 7 | resolve(response.ok); 8 | }); 9 | -------------------------------------------------------------------------------- /src/services/config-variables.service.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import { substituteConfigVariables } from './config-variables.service'; 3 | 4 | describe('substitute config variables', () => { 5 | it('should replace $values with real values', () => { 6 | const configuration = { 7 | env: { cwd: '$projectPath', PORT: '$port' }, 8 | create: ['npx', '$projectPath', '$projectStarter'], 9 | }; 10 | 11 | // Flow error here & not sure why. 12 | // It complains about missing property toMatchInlineSnapshot with 6 or cases. 13 | // It error message starts like: 14 | // Cannot call `expect(...).toMatchInlineSnapshot` because: 15 | // - Either property`toMatchInlineSnapshot` is missing in `JestExpectType`[1]. 16 | // - Or property `toMatchInlineSnapshot` is missing in `JestPromiseType` [2]. 17 | // - ... 18 | // $FlowFixMe 19 | expect( 20 | substituteConfigVariables(configuration, { 21 | $port: '3000', 22 | $projectPath: 'some/path/to/project', 23 | $projectStarter: 'https://github.com/gatsbyjs/gatsby-starter-default', 24 | }) 25 | ).toMatchInlineSnapshot(` 26 | Object { 27 | "create": Array [ 28 | "npx", 29 | "some/path/to/project", 30 | "https://github.com/gatsbyjs/gatsby-starter-default", 31 | ], 32 | "env": Object { 33 | "PORT": "3000", 34 | "cwd": "some/path/to/project", 35 | }, 36 | } 37 | `); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/services/create-project.fixtures.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Fake data for manually testing project-creation flow. 3 | export const FAKE_CRA_PROJECT = { 4 | name: 'haidddd', 5 | version: '0.1.0', 6 | private: true, 7 | dependencies: { 8 | react: '^16.4.0', 9 | 'react-dom': '^16.4.0', 10 | 'react-scripts': '1.1.4', 11 | }, 12 | scripts: { 13 | start: 'react-scripts start', 14 | build: 'react-scripts build', 15 | test: 'react-scripts test --env=jsdom', 16 | eject: 'react-scripts eject', 17 | }, 18 | guppy: { 19 | id: 'haidddd', 20 | name: 'Haidddd', 21 | icon: '/static/media/icon_blueorange.174c0078.jpg', 22 | color: '#FFF', 23 | createdAt: Date.now(), 24 | type: 'gatsby', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/services/electron-store.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Need to use commonjs so that `main.js` can use this module as well. 3 | const ElectronStore = require('electron-store'); 4 | 5 | // Expose a singleton store instance. 6 | module.exports = new ElectronStore(); 7 | -------------------------------------------------------------------------------- /src/services/find-available-port.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Find a clear port to run a server on. 4 | * 5 | * NOTE: Initially, we tried to copy create-react-app's approach, using the 6 | * `detect-port-alt` NPM package. For some reason, maybe involving electron, 7 | * that module didn't work; it would create a test server, but they'd hang; 8 | * no errors called, but no listeners called either. 9 | * 10 | * Instead, we're using platform-specific OS tools: 11 | * - `lsof` on Mac/Linux 12 | * - `netstat` on Windows 13 | */ 14 | import * as childProcess from 'child_process'; 15 | import { isWin } from './platform.service'; 16 | 17 | const MAX_ATTEMPTS = 15; 18 | 19 | export default () => 20 | new Promise((resolve, reject) => { 21 | const checkPort = (port = 3000, attemptNum = 0) => { 22 | // For Windows Support 23 | // Similar command to lsof 24 | // Finds if the specified port is in use 25 | const command = isWin 26 | ? `netstat -aon | find "${port}"` 27 | : `lsof -i :${port}`; 28 | const env = isWin 29 | ? { 30 | cwd: 'C:\\Windows\\System32', 31 | } 32 | : undefined; 33 | childProcess.exec(command, env, (err, res) => { 34 | // Ugh, childProcess assumes that no output means that there was an 35 | // error, and `lsof` emits nothing when the port is empty. So, 36 | // counterintuitively, an error is good news, and a response is bad. 37 | if (res) { 38 | if (attemptNum > MAX_ATTEMPTS) { 39 | reject(`No available ports after ${MAX_ATTEMPTS} attempts.`); 40 | } 41 | 42 | checkPort(port + 1, attemptNum + 1); 43 | return; 44 | } 45 | 46 | resolve(port); 47 | }); 48 | }; 49 | 50 | checkPort(); 51 | }); 52 | -------------------------------------------------------------------------------- /src/services/platform.service.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import path from 'path'; 3 | 4 | import { getBaseProjectEnvironment, isWin } from './platform.service'; 5 | 6 | const pathKey = isWin ? 'Path' : 'PATH'; 7 | 8 | describe('Platform service', () => { 9 | describe('getBaseProjectEnvironment', () => { 10 | it('returns a valid PATH', () => { 11 | const baseEnv = getBaseProjectEnvironment('hello-world', {}); 12 | 13 | expect(baseEnv[pathKey]).toBeTruthy(); 14 | expect( 15 | baseEnv[pathKey].indexOf( 16 | path.join('hello-world', 'node_modules', '.bin') 17 | ) 18 | ).toBeGreaterThan(0); 19 | }); 20 | 21 | it('includes FORCE_COLOR: true', () => { 22 | const baseEnv = getBaseProjectEnvironment('hello-world', {}); 23 | 24 | expect(baseEnv.FORCE_COLOR).toBe(true); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/services/process-logger.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { LOGGING } from '../config/app'; 3 | 4 | import type { ChildProcess } from 'child_process'; 5 | 6 | export const processLogger = (child: ChildProcess, label: string) => { 7 | if (!LOGGING || process.env.NODE_ENV === 'production') { 8 | return; // no logging 9 | } 10 | 11 | if (!child.stdout) { 12 | return; // needed during tests 13 | } 14 | 15 | // Todo: Handle color codes in logging to console (if supported). There are many control characters in the console output. 16 | child.stdout.on('data', data => { 17 | // data is an uint8 array --> decode to string 18 | console.log('[%s]: %s', label, data.toString()); 19 | }); 20 | 21 | child.stderr.on('data', data => { 22 | console.error('[%s]: %s', label, data.toString()); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/project-name-service.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import { generateRandomName, prefixes, suffixes } from './project-name.service'; 3 | 4 | describe('generateRandomName', () => { 5 | const projectName = generateRandomName(); 6 | const tokens = projectName.split(' '); 7 | 8 | it('should capitalize the first letter of each word in the project name', () => { 9 | tokens.map(token => () => expect(token.charAt(0)).stringMatching(/[A-Z]/)); 10 | }); 11 | 12 | it('should put together a prefix and a suffix to generate a project name', () => { 13 | // The list of words are lowercase, therefore we need to make sure our comparison is the same 14 | // If the list ever changes to not be all lowercase, then this test will need to be changed/rewritten 15 | const firstWord = tokens[0].toLowerCase(); 16 | const lastWord = tokens[tokens.length - 1].toLowerCase(); 17 | 18 | // We're assuming there are only two words per project name 19 | // If there are more, we're only testing if the first word is contained within prefix list 20 | // and if the last word is contained in suffix list 21 | const isFirstWordAPrefix = prefixes.includes(firstWord); 22 | const isLastWordASuffix = suffixes.includes(lastWord); 23 | 24 | expect(isFirstWordAPrefix).toEqual(true); 25 | expect(isLastWordASuffix).toEqual(true); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/services/project-type-specifics.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Guppy currently supports two project types: 4 | * - create-react-app 5 | * - Gatsby 6 | * 7 | * While things are mostly the same for these project types, they do vary in 8 | * a handful of ways. This file should collect the bits that vary across 9 | * project types. 10 | */ 11 | import type { ProjectType } from '../types'; 12 | 13 | export const getDocumentationLink = (projectType: ProjectType) => { 14 | switch (projectType) { 15 | case 'create-react-app': 16 | return 'https://github.com/facebook/create-react-app#user-guide'; 17 | case 'gatsby': 18 | return 'https://www.gatsbyjs.org/docs/'; 19 | case 'nextjs': 20 | return 'https://nextjs.org/docs/'; 21 | default: 22 | throw new Error('Unrecognized project type: ' + projectType); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/services/project-type-specifics.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable flowtype/require-valid-file-annotation */ 2 | import { getDocumentationLink } from './project-type-specifics'; 3 | 4 | describe('getDocumentationLink', () => { 5 | it('should get the documentation links by project type', () => { 6 | const gatsbyString = getDocumentationLink('gatsby'); 7 | const createReactAppString = getDocumentationLink('create-react-app'); 8 | 9 | expect(typeof gatsbyString).toEqual('string'); 10 | expect(typeof createReactAppString).toEqual('string'); 11 | 12 | expect(gatsbyString).not.toBe(createReactAppString); 13 | }); 14 | 15 | it('should throw an exception if passed a project type that is not defined', () => { 16 | const unknownProjectType = 'some-unknown-project-type'; 17 | 18 | expect(() => getDocumentationLink(unknownProjectType)).toThrow( 19 | `Unrecognized project type: ${unknownProjectType}` 20 | ); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/services/shell.service.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * Thin wrapper around electron `shell`, and other shell-like functions 4 | */ 5 | import { shell, remote } from 'electron'; 6 | import launchEditor from 'react-dev-utils/launchEditor'; 7 | import { exec } from 'child_process'; 8 | 9 | import type { Project } from '../types'; 10 | 11 | const { BrowserWindow } = remote; 12 | 13 | const openedWindows = {}; 14 | 15 | export const openProjectInFolder = (project: Project) => 16 | shell.openItem(project.path); 17 | 18 | export const openProjectInEditor = (projectOrPath: Project | string) => 19 | launchEditor(projectOrPath.path || projectOrPath, 1, 1); 20 | 21 | export const openWindow = (url: string) => { 22 | const urlKey = url.replace(/(\/|:|\.)/g, ''); 23 | 24 | if (openedWindows[urlKey]) { 25 | try { 26 | return openedWindows[urlKey].show(); 27 | } catch (err) { 28 | if (!err.message.includes('Object has been destroyed')) { 29 | console.error(err); 30 | throw new Error('Unhandled error'); 31 | } 32 | // else swallow Object has been destroyed - Window could be closed 33 | } 34 | } 35 | 36 | const win = new BrowserWindow({ 37 | width: 800, 38 | height: 600, 39 | webPreferences: { 40 | devTools: false, 41 | }, 42 | }); 43 | // Remove the menu 44 | win.setMenu(null); 45 | win.loadURL(url); 46 | win.show(); 47 | 48 | // Save Window so we can open it later 49 | openedWindows[urlKey] = win; 50 | }; 51 | 52 | export const getNodeJsVersion = () => 53 | new Promise(resolve => 54 | exec('node -v', { env: window.process.env }, (error, stdout) => { 55 | if (error) { 56 | return resolve(); 57 | } 58 | 59 | resolve(stdout.toString().trim()); 60 | }) 61 | ); 62 | -------------------------------------------------------------------------------- /src/store/storage-engine.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /** 3 | * This is a modified version of redux-storage-engine-electron-store 4 | * (https://github.com/collmot/redux-storage-engine-electron-store) 5 | * 6 | * This fork does two things: 7 | * - Exposes the created Electron store on the `window`, for debugging 8 | * - Simplifies a bit by assuming a `key` will be provided, and no 9 | * modifications to the store are required. 10 | */ 11 | 12 | import electronStore from '../services/electron-store.service'; 13 | 14 | function rejectWithMessage(error) { 15 | return Promise.reject(error.message); 16 | } 17 | 18 | export default function createEngine(key: string) { 19 | window.electronStore = electronStore; 20 | 21 | return { 22 | load: () => 23 | new Promise(resolve => { 24 | resolve(electronStore.get(key) || {}); 25 | }).catch(rejectWithMessage), 26 | 27 | save: (state: any) => 28 | new Promise(resolve => { 29 | electronStore.set(key, state); 30 | }).catch(rejectWithMessage), 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/stories/colors.stories.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Fragment } from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import styled from 'styled-components'; 5 | 6 | import Heading from '../components/Heading'; 7 | import { contrastingColor } from '../utils'; 8 | import { RAW_COLORS, COLORS, GRADIENTS } from '../constants'; 9 | 10 | const ColorList = ({ colors }) => ( 11 | 12 | {Object.entries(colors).map( 13 | ([name, gradient], i) => 14 | typeof gradient === 'string' ? ( 15 | 16 | {name} 17 | 18 | ) : Array.isArray(gradient) ? ( 19 | 20 | {name} 21 | 22 | ) : ( 23 | 24 | 27 | {name} 28 | 29 | {Object.entries(gradient).map(([interval, color], j) => ( 30 | 31 | {interval} 32 | 33 | ))} 34 | 35 | ) 36 | )} 37 | 38 | ); 39 | 40 | storiesOf('Colors', module) 41 | .add('Semantics', () => ) 42 | .add('Gradients', () => ) 43 | .add('All', () => ); 44 | 45 | const ColorBlock = styled.div` 46 | background: ${props => props.color}; 47 | border-radius: 4px; 48 | color: ${props => contrastingColor(props.color)}; 49 | width: 100%; 50 | padding: 10px; 51 | margin-bottom: 10px; 52 | `; 53 | 54 | const GradientBlock = styled.div` 55 | background-image: linear-gradient(15deg, ${props => props.colors.join(', ')}); 56 | border-radius: 4px; 57 | color: ${props => contrastingColor(props.colors[0])}; 58 | width: 100%; 59 | padding: 10px; 60 | margin-bottom: 10px; 61 | `; 62 | --------------------------------------------------------------------------------