diff --git a/packages/app/package.json b/packages/app/package.json index 8ddb5f52456..ebc92b71a23 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -72,6 +72,7 @@ "@codesandbox/executors": "^0.1.0", "@codesandbox/template-icons": "^1.0.1", "@emmetio/codemirror-plugin": "^0.3.5", + "@samuelmeuli/font-manager": "^1.2.0", "@sentry/webpack-plugin": "^1.8.0", "@styled-system/css": "^5.0.23", "@svgr/core": "^2.4.1", diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/List.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/List.tsx new file mode 100644 index 00000000000..aa3d63e251c --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/List.tsx @@ -0,0 +1,60 @@ +import React, { FunctionComponent, useState, useMemo } from 'react'; +import { Font } from '@samuelmeuli/font-manager'; +import { List, SearchFonts, FontLI, FontFamily, Arrow } from './elements'; + +type Props = { + fonts: Font[]; + onSelection: (e: any) => void; + activeFontFamily: string; + expanded: boolean; +}; + +export const FontList: FunctionComponent = ({ + fonts, + onSelection, + activeFontFamily, + expanded, +}) => { + const [searchTerm, setSearchTerm] = useState(''); + + const updateSearch = (e: any) => setSearchTerm(e.target.value); + + const getFontId = (fontFamily: string): string => + fontFamily.replace(/\s+/g, '-').toLowerCase(); + + const getFonts: Font[] = useMemo( + () => + fonts.filter(f => + f.family.toLowerCase().includes(searchTerm.trim().toLowerCase()) + ), + [fonts, searchTerm] + ); + return ( + <> + + + + {getFonts.map((font: Font) => ( + + + {font.family} + + + ))} + + + ); +}; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/elements.ts b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/elements.ts new file mode 100644 index 00000000000..3a8753230d3 --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/elements.ts @@ -0,0 +1,138 @@ +import styled, { css } from 'styled-components'; +import Color from 'color'; + +const makeDarker = ({ theme }) => + Color(theme['input.background']) + .darken(theme.light ? 0.1 : 0.3) + .rgbString(); + +const makeLighter = ({ theme }) => + Color(theme['sideBar.background']) + .lighten(0.3) + .rgbString(); + +export const SearchFonts = styled.input` + border: 1px solid ${props => makeDarker(props)}; + box-sizing: border-box; + border-radius: 2px; + width: 100%; + margin-bottom: 0.5rem; + padding: 0.5rem; + background: transparent; + color: ${props => + props.theme['input.foreground'] || + (props.theme.light ? '#636363' : 'white')}; + + ::-webkit-input-placeholder { + color: ${props => + props.theme['input.foreground'] || + (props.theme.light ? '#636363' : 'white')}; + } + ::-moz-placeholder { + color: ${props => + props.theme['input.foreground'] || + (props.theme.light ? '#636363' : 'white')}; + } + :-ms-input-placeholder { + color: ${props => + props.theme['input.foreground'] || + (props.theme.light ? '#636363' : 'white')}; + } +`; + +export const FontFamily = styled.button<{ active?: boolean }>` + margin: 0; + padding: 0; + background-color: ${props => makeLighter(props)}; + width: 100%; + padding-left: 0.25rem; + border: none; + text-align: left; + color: ${props => props.theme['sideBar.foreground'] || 'inherit'}; + cursor: pointer; + + &:focus { + border-color: ${props => props.theme.secondary.clearer(0.6)}; + } +`; + +export const FontLI = styled.li` + color: ${props => props.theme['sideBar.foreground'] || 'inherit'}; + padding: 0.5rem; + text-align: left; + cursor: pointer; +`; + +export const List = styled.ul<{ expanded?: boolean }>` + font-size: 13px; + list-style: none; + border: 1px solid ${props => makeDarker(props)}; + box-sizing: border-box; + padding: 0.5rem; + margin: 0; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.24), 0px 4px 4px rgba(0, 0, 0, 0.12); + max-height: 0; + overflow: scroll; + transition: all 200ms ease; + text-align: left; + display: none; + margin-top: 0.5rem; + background-color: ${props => makeLighter(props)}; + width: 240px; + z-index: 10; + + ${props => + props.expanded && + css` + max-height: 130px; + display: block; + `} +`; + +export const SelectedFont = styled.button<{ done?: boolean }>` + background-color: ${props => + props.theme['input.background'] || 'rgba(0, 0, 0, 0.3)'}; + color: ${props => + props.theme['input.foreground'] || + (props.theme.light ? '#636363' : 'white')}; + border: 1px solid ${props => makeDarker(props)}; + + box-shadow: none; + text-align: left; + appearance: none; + width: 100%; + padding: 0.5rem 0.75rem; + position: relative; + box-sizing: border-box; + outline: none; + + ${props => + props.done && + css` + :after { + content: ''; + background-image: url("${`data:image/svg+xml,%3Csvg width='7' height='4' viewBox='0 0 7 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.50007 4L1.27146e-07 1.35122e-07L7 -4.76837e-07L3.50007 4Z' fill='${ + props.theme.light ? 'black' : 'white' + }'/%3E%3C/svg%3E%0A`}"); + width: 7px; + height: 4px; + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + } + `} +`; + +export const Arrow = styled.div` + width: 0; + height: 0; + border-style: solid; + border-width: 0 6px 6px 6px; + border-color: transparent transparent + ${props => props.theme['sideBar.background']} transparent; + position: absolute; + margin-top: 2px; + left: 50%; + margin-left: -6px; +`; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/index.tsx new file mode 100644 index 00000000000..46cfc3d1a4b --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/FontPicker/index.tsx @@ -0,0 +1,102 @@ +import { + FontManager, + Font, + FONT_FAMILY_DEFAULT, + OPTIONS_DEFAULTS, +} from '@samuelmeuli/font-manager'; +import React, { useState, useEffect, useRef } from 'react'; +import OutsideClickHandler from 'react-outside-click-handler'; +import { Portal } from 'reakit'; +import { SelectedFont } from './elements'; +import { FontList } from './List'; + +export const FontPicker = ({ + activeFontFamily = FONT_FAMILY_DEFAULT, + onChange, + apiKey, +}) => { + const [expanded, setExpanded] = useState(false); + const [loadingStatus, setLoadingStatus] = useState('loading'); + const [fontManager, setFontManager] = useState(); + const [style, setStyle] = useState({ x: 0, y: 0 }); + const ref = useRef(null); + + useEffect(() => { + const manager = new FontManager( + apiKey, + activeFontFamily, + { ...OPTIONS_DEFAULTS, limit: 200 }, + onChange + ); + setFontManager(manager); + + manager + .init() + .then(() => setLoadingStatus('finished')) + .catch((err: Error) => { + setLoadingStatus('error'); + console.error(err); + }); + // eslint-disable-next-line + }, []); + + useEffect(() => { + if (ref && ref.current) { + const styles = ref.current.getBoundingClientRect(); + setStyle({ + x: styles.x, + y: styles.y, + }); + } + }, [ref]); + + const onSelection = (e: any): void => { + const active = e.target.textContent; + if (!active) { + throw Error(`Missing font family in clicked font button`); + } + fontManager.setActiveFont(active); + toggleExpanded(); + }; + + const toggleExpanded = () => setExpanded(exp => !exp); + + const fonts: Font[] = + fontManager && Array.from(fontManager.getFonts().values()); + + return ( + <> + + {loadingStatus === 'loading' ? 'Loading Typefaces' : activeFontFamily} + + {expanded && loadingStatus === 'finished' && ( + +
+ setExpanded(false)}> + + +
+
+ )} + + ); +}; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/elements.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/elements.js new file mode 100644 index 00000000000..9810ee4a7fb --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/elements.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + margin: 0.5rem 1rem; + position: relative; +`; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/index.js new file mode 100644 index 00000000000..a400d21f7fc --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddFont/index.js @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import { Button } from '@codesandbox/common/lib/components/Button'; +import { FontPicker } from './FontPicker/index'; +import { Container } from './elements'; + +export const AddFont = ({ addResource }) => { + const [activeFontFamily, setActiveFontFamily] = useState('Roboto'); + + const addFont = async () => { + if (activeFontFamily) { + const font = activeFontFamily.trim().replace(/ /g, '+'); + const link = `https://fonts.googleapis.com/css?family=${font}&display=swap`; + await addResource(link); + } + }; + + return ( + <> + + setActiveFontFamily(nextFont.family)} + /> + + + + + + ); +}; diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddResource/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddResource/index.js index e8e6576c077..f3aff8009f0 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddResource/index.js +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/AddResource/index.js @@ -9,7 +9,7 @@ const initialState = { name: '', }; -export default class AddVersion extends React.PureComponent { +export class AddResource extends React.PureComponent { state = initialState; setName = e => { diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalFonts/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalFonts/index.js new file mode 100644 index 00000000000..04226ce7965 --- /dev/null +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalFonts/index.js @@ -0,0 +1,37 @@ +import React from 'react'; + +import CrossIcon from 'react-icons/lib/md/clear'; + +import { EntryContainer, IconArea, Icon } from '../../elements'; +import { Link } from '../elements'; + +const getFamily = search => { + const hashes = search.slice(search.indexOf('?') + 1).split('&'); + const family = hashes + .find(hash => hash.split('=')[0] === 'family') + .split('=')[1]; + + return { + name: family.split('+').join(' '), + id: family + .split('+') + .join('-') + .toLowerCase(), + }; +}; + +export const ExternalFonts = ({ removeResource, resource }) => ( + + + {getFamily(resource).name} + + + removeResource(resource)} + > + + + + +); diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.js b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.tsx similarity index 82% rename from packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.js rename to packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.tsx index 8a41c86302a..b6929cc5b66 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.js +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/ExternalResource/index.tsx @@ -5,6 +5,11 @@ import CrossIcon from 'react-icons/lib/md/clear'; import { EntryContainer, IconArea, Icon } from '../../elements'; import { Link } from '../elements'; +interface IExternalResource { + removeResource: (a: any) => void; + resource: any; +} + const getNormalizedUrl = (url: string) => `${url.replace(/\/$/g, '')}/`; function getName(resource: string) { @@ -18,7 +23,7 @@ function getName(resource: string) { return getNormalizedUrl(resource); } -export default class ExternalResource extends React.PureComponent { +export class ExternalResource extends React.PureComponent { removeResource = () => { this.props.removeResource(this.props.resource); }; @@ -26,7 +31,7 @@ export default class ExternalResource extends React.PureComponent { render() { const { resource } = this.props; return ( - + {getName(resource)} diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/index.tsx index 8c768eabed8..fcc1a036392 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/index.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/Dependencies/index.tsx @@ -8,8 +8,10 @@ import { WorkspaceSubtitle } from '../elements'; import { AddVersion } from './AddVersion'; import { VersionEntry } from './VersionEntry'; -import AddResource from './AddResource'; -import ExternalResource from './ExternalResource'; +import { AddResource } from './AddResource'; +import { AddFont } from './AddFont'; +import { ExternalResource } from './ExternalResource'; +import { ExternalFonts } from './ExternalFonts'; import { ErrorMessage } from './elements'; @@ -29,10 +31,7 @@ export const Dependencies: FunctionComponent = () => { if (error) { return ( - - We weren - {"'"}t able to parse the package.json - + We weren{"'"}t able to parse the package.json ); } @@ -40,6 +39,12 @@ export const Dependencies: FunctionComponent = () => { // const devDependencies = parsed.devDependencies || {}; const templateDefinition = getDefinition(sandbox.template); + const fonts = sandbox.externalResources.filter(resource => + resource.includes('fonts.googleapis.com/css') + ); + const otherResources = sandbox.externalResources.filter( + resource => !resource.includes('fonts.googleapis.com/css') + ); return (
@@ -61,30 +66,37 @@ export const Dependencies: FunctionComponent = () => { /> ))} {/* {Object.keys(devDependencies).length > 0 && ( - Development Dependencies - )} - {Object.keys(devDependencies) - .sort() - .map(dep => ( - signals.editor.npmDependencyRemoved({ name })} - onRefresh={(name, version) => - signals.editor.addNpmDependency({ - name, - version, - }) - } - /> - ))} */} + Development Dependencies + )} + {Object.keys(devDependencies) + .sort() + .map(dep => ( + signals.editor.npmDependencyRemoved({ name })} + onRefresh={(name, version) => + signals.editor.addNpmDependency({ + name, + version, + }) + } + /> + ))} */} Add Dependency {templateDefinition.externalResourcesEnabled && (
External Resources - {(sandbox.externalResources || []).map(resource => ( + + workspace.externalResourceAdded({ + resource, + }) + } + /> + {otherResources.map(resource => ( { } /> ))} - Google Fonts + + workspace.externalResourceAdded({ resource, }) } /> + {fonts.map(resource => ( + + workspace.externalResourceRemoved({ + resource, + }) + } + /> + ))}
)}
diff --git a/packages/app/src/app/pages/Sandbox/Editor/Workspace/WorkspaceItem/index.tsx b/packages/app/src/app/pages/Sandbox/Editor/Workspace/WorkspaceItem/index.tsx index dd104ca7574..823cb9bbcfb 100644 --- a/packages/app/src/app/pages/Sandbox/Editor/Workspace/WorkspaceItem/index.tsx +++ b/packages/app/src/app/pages/Sandbox/Editor/Workspace/WorkspaceItem/index.tsx @@ -70,6 +70,7 @@ export class WorkspaceItem extends React.Component { transitionOnMount start={{ height: 0, // The starting style for the component. + // If the 'leave' prop isn't defined, 'start' is reused! }} show={open} diff --git a/packages/app/src/sandbox/external-resources.js b/packages/app/src/sandbox/external-resources.ts similarity index 88% rename from packages/app/src/sandbox/external-resources.js rename to packages/app/src/sandbox/external-resources.ts index c854864a354..cd2d9b84baf 100644 --- a/packages/app/src/sandbox/external-resources.js +++ b/packages/app/src/sandbox/external-resources.ts @@ -4,10 +4,12 @@ function getExternalResourcesConcatenation(resources: Array) { /* eslint-disable no-cond-assign */ function clearExternalResources() { let el = null; + // eslint-disable-next-line no-cond-assign while ((el = document.getElementById('external-css'))) { el.remove(); } + // eslint-disable-next-line no-cond-assign while ((el = document.getElementById('external-js'))) { el.remove(); } @@ -40,7 +42,10 @@ function addJS(resource: string) { function addResource(resource: string) { const match = resource.match(/\.([^.]*)$/); - const el = match && match[1] === 'css' ? addCSS(resource) : addJS(resource); + const el = + (match && match[1] === 'css') || resource.includes('fonts.googleapis') + ? addCSS(resource) + : addJS(resource); return new Promise(r => { el.onload = r; diff --git a/yarn.lock b/yarn.lock index e1150405e36..3a273a9ea54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3019,6 +3019,13 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" +"@samuelmeuli/font-manager@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@samuelmeuli/font-manager/-/font-manager-1.2.0.tgz#ec9ec3dab4cbbd56ce2f579f332bbd7e3aa26894" + integrity sha512-ChNcTdVDL0OjXAMQYi8R9ScXvVZ3p32/4MupKbGksGtF1xPKKTM6vEo74XCBo7LEPQ+Eb+BiS9yD4B0Oo4/LQw== + dependencies: + "@babel/runtime" "^7.5.5" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -21212,7 +21219,7 @@ react-dom@16.8.1: prop-types "^15.6.2" scheduler "^0.13.1" -react-dom@16.9.0, react-dom@^16.2.0, react-dom@^16.8.3, react-dom@^16.9.0: +react-dom@16.9.0, react-dom@^16.2.0, react-dom@^16.8.3, react-dom@^16.8.6, react-dom@^16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962" integrity sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==