├── .all-contributorsrc ├── .eslintrc.js ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── other.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── ranger.yml └── workflows │ └── validate.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .vercelignore ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── deploy.sh ├── components ├── Announcement.js ├── ApiContext.js ├── AuthContext.js ├── BackgroundSelect.js ├── Billing.js ├── Button.js ├── Carbon.js ├── ColorPicker.js ├── ConfirmButton.js ├── CopyMenu.js ├── Dropdown.js ├── Editor.js ├── EditorContainer.js ├── ExportMenu.js ├── FontFace.js ├── FontSelect.js ├── Footer.js ├── Header.js ├── ImagePicker.js ├── Input.js ├── ListSetting.js ├── LoginButton.js ├── MenuButton.js ├── Meta.js ├── Overlay.js ├── Page.js ├── PhotoCredit.js ├── Popout.js ├── Presets.js ├── RandomImage.js ├── ReferralLink.js ├── SelectionEditor.js ├── Settings.js ├── ShareMenu.js ├── Slider.js ├── SnippetToolbar.js ├── Spinner.js ├── ThemeSelect.js ├── Themes │ ├── GlobalHighlights.js │ ├── ThemeCreate.js │ └── index.js ├── Toasts.js ├── Toggle.js ├── Toolbar.js ├── WidthHandler.js ├── WindowControls.js ├── WindowPointer.js ├── hooks.js ├── style │ ├── Font.js │ ├── Reset.js │ └── Typography.js └── svg │ ├── Arrows.js │ ├── Checkmark.js │ ├── Controls.js │ ├── Copy.js │ ├── Language.js │ ├── Logo.js │ ├── Remove.js │ ├── Settings.js │ ├── Theme.js │ ├── Watermark.js │ └── WindowThemes.js ├── cypress ├── config.json ├── integration │ ├── background-color.spec.js │ ├── basic.spec.js │ ├── embed.spec.js │ ├── gist.spec.js │ ├── localStorage.spec.js │ ├── security.spec.js │ └── visual-testing.spec.js ├── plugins │ └── index.js ├── support │ └── index.js └── util.js ├── docs ├── README.ar.md ├── README.bn.md ├── README.br.pt.md ├── README.cn.zh.md ├── README.de.md ├── README.es.md ├── README.fa.md ├── README.fr.md ├── README.he.md ├── README.hi.md ├── README.in.md ├── README.it.md ├── README.ja.md ├── README.kr.md ├── README.ml.md ├── README.nl.md ├── README.pl.md ├── README.ru.md ├── README.se.md ├── README.ta.md ├── README.tr.md ├── README.tw.zh.md ├── README.ua.md └── README.uz.md ├── lib ├── api.js ├── client.js ├── constants.js ├── custom │ ├── autoCloseBrackets.js │ └── modes │ │ ├── apache.js │ │ ├── elixir.js │ │ ├── graphql.js │ │ ├── nim.js │ │ ├── riscv.js │ │ └── solidity.js ├── dom-to-image.js ├── highlight-languages.js ├── routing.js └── util.js ├── next.config.js ├── package.json ├── pages ├── [id].js ├── _document.js ├── about.js ├── account.js ├── api │ ├── image │ │ └── [id].js │ └── oembed.js ├── embed │ ├── [id].js │ └── index.js ├── index.js └── snippets.js ├── public ├── favicon.ico ├── manifest.json ├── robots.txt └── static │ ├── brand │ ├── apple-touch-icon.png │ ├── banner.png │ ├── desktop.png │ ├── icon.png │ ├── logo-banner-transparent.png │ ├── logo-banner.png │ ├── logo-square.png │ └── social-banner.png │ ├── presets │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png │ ├── react-crop.css │ ├── svg │ ├── github.svg │ ├── open-source-companies-2.svg │ ├── open-source-companies.svg │ ├── person.svg │ └── snippets.svg │ └── themes │ ├── night-owl.min.css │ ├── nord.min.css │ ├── one-dark.min.css │ ├── one-light.min.css │ ├── synthwave-84.min.css │ └── verminal.min.css ├── release.js ├── vercel.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { es6: true, jest: true }, 3 | extends: ['eslint:recommended', 'plugin:jsx-a11y/recommended', 'next'], 4 | rules: { 5 | 'import/no-unresolved': 'error', 6 | 'no-duplicate-imports': 'error', 7 | 'react/display-name': 'off', 8 | 'react/jsx-no-target-blank': 'error', 9 | 'react/jsx-uses-react': 'error', 10 | 'react/jsx-uses-vars': 'error', 11 | 'jsx-a11y/click-events-have-key-events': 'off', 12 | 'react-hooks/rules-of-hooks': 'error', 13 | 'react-hooks/exhaustive-deps': 'error', 14 | 'no-console': ['error', { allow: ['error'] }], 15 | // TODO re-enable these 16 | '@next/next/no-img-element': 'off', 17 | '@next/next/no-html-link-for-pages': 'off', 18 | '@next/next/link-passhref': 'off', 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at carbon.now.sh@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you have discovered a bug or have a feature suggestion, feel free to create an issue on GitHub. You don't need to be assigned an issue in order to work on it—consider all issues free rein. 4 | 5 | If you'd like to make some changes yourself, see the following: 6 | 7 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device 8 | 1. Make sure yarn is globally installed (`npm install -g yarn`) 9 | 1. Run `yarn` to download required packages 10 | 1. Build and start the application: `yarn dev` 11 | 1. Finally, submit a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) with your changes! 12 | 13 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification, and uses the [@all-contributors](https://allcontributors.org/docs/en/bot/usage) bot to add new contributors. 14 | 15 | Contributions of any kind are welcome! 16 | 17 | ### Adding themes/languages/fonts 18 | 19 | We are not currently accepting new themes, languages, or fonts into Carbon, except for in extenuating circumstances. Instead, we want to continue to provide ways for users to add their own themes and presets. Please feel free to still open an issue or PR for consideration, but know that there is a chance it will get closed without addition. 20 | 21 | ## Stats 22 | ![Alt](https://repobeats.axiom.co/api/embed/471ed135120d0b6c3ec17fa4e8aa371c9173bd87.svg "Repobeats analytics image") 23 | 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [carbon-app, mfix22] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 2 | open_collective: # carbon-app/contribute 3 | patreon: # Replace with a single Patreon username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | otechie: # Replace with a single Otechie username 10 | custom: [] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help fix something about Carbon 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '…' 17 | 2. Click on '…' 18 | 3. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Info (please complete the following information):** 27 | 28 | - OS [e.g. macOS, Linux, Windows, iOS]: 29 | - Browser [e.g. Chrome, Safari, Firefox]: 30 | - Carbon URL [e.g. carbon.now.sh?bg=pink]: 31 | 32 | 33 |
34 | Code snippet 35 |
36 |   
37 |   
38 |
39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: Let us know about something else 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the change** 11 | A clear and concise description of your proposal. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain the issue. 15 | 16 | **Info (please complete the following information):** 17 | 18 | - OS [e.g. macOS, Linux, Windows, iOS]: 19 | - Browser [e.g. Chrome, Safari, Firefox]: 20 | 21 |
22 | Code snippet 23 |
24 |   
25 |   
26 |
27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | - [ ] Integration tests (if applicable) 9 | 10 | Closes # 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Configuration for npm 4 | - package-ecosystem: 'npm' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | allow: 9 | dependency-type: 'development' 10 | # labels: 11 | # - 'dependencies' 12 | # ignore: 13 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-update 14 | -------------------------------------------------------------------------------- /.github/ranger.yml: -------------------------------------------------------------------------------- 1 | _extends: reporanger/superpowers 2 | 3 | labels: 4 | dependencies: merge 5 | contributor: 6 | action: comment 7 | delay: 0s 8 | message: '@all-contributors add @$AUTHOR for code' 9 | translator: 10 | action: comment 11 | delay: 0s 12 | message: '@all-contributors add @$AUTHOR for translation' 13 | duplicate: 14 | action: close 15 | delay: 2 days 16 | comment: ⚠️ This has been marked to be closed in $DELAY. 17 | 'theme/language': 18 | action: close 19 | delay: 3 days 20 | comment: | 21 | This issue has been marked "$LABEL". As of Carbon `4.0.0`, the Carbon core team is no longer implementing new themes or languages, except for in extenuating circumstances. You can create your own theme in the "Themes" dropdown. 22 | 23 | This issue will remain open for $DELAY for further consideration. 24 | 25 | commits: 26 | - action: label 27 | pattern: 'upgrade dep' 28 | labels: 29 | - maintenance 30 | - action: label 31 | user: 'allcontributors[bot]' 32 | labels: 33 | - 'squash when passing' 34 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Use Node.js 11 | uses: actions/setup-node@v3 12 | - name: npm install, lint, build, and test 13 | run: | 14 | yarn 15 | npm run lint --if-present 16 | npm run build --if-present 17 | npm start & npx wait-on http://localhost:3000 && npm run cy:run -- --config baseUrl=http://localhost:3000 18 | # --record --key 26c0b9eb-40f9-4ca6-b91d-a39f03652011 19 | env: 20 | CI: true 21 | CYPRESS_CI: true 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | .next 4 | dist 5 | out 6 | cypress/videos 7 | cypress/screenshots 8 | .idea 9 | .DS_Store 10 | packaged 11 | coverage 12 | public/service-worker.js 13 | private-key.json 14 | .now 15 | .vercel 16 | *.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | registry=https://registry.npmjs.org 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "semi": false, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .github 2 | LICENSE 3 | README.md 4 | bin 5 | node_modules 6 | cypress 7 | cypress.json 8 | docs 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Carbon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please contact us at [carbon.now.sh+security@gmail.com](mailto:carbon.now.sh+security@gmail.com) to privately inform us of any security vulnerability or issue. 4 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | vercel switch carbon-app 5 | 6 | VERCEL_URL=$(vercel) 7 | 8 | yarn cy:run --config baseUrl="$VERCEL_URL" 9 | 10 | echo "$VERCEL_URL"| tee /dev/tty | pbcopy 11 | 12 | read -p "Deploy to production (y/N)?" -r 13 | echo 14 | if [[ $REPLY =~ ^[Yy]$ ]] 15 | then 16 | vercel alias "$VERCEL_URL" carbon.now.sh 17 | fi -------------------------------------------------------------------------------- /components/Announcement.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Feature flag 4 | const ACTIVE = false 5 | 6 | const key = 'CARBON_CTA_4' 7 | 8 | function Toast() { 9 | const [open, setState] = React.useState(false) 10 | 11 | React.useEffect(() => { 12 | window.localStorage.removeItem('CARBON_CTA_2') 13 | window.localStorage.removeItem('CARBON_CTA_3') 14 | if (!window.localStorage.getItem(key)) { 15 | setState(true) 16 | } 17 | }, []) 18 | 19 | if (process.env.NODE_ENV !== 'production') { 20 | return null 21 | } 22 | 23 | if (!ACTIVE) { 24 | return null 25 | } 26 | 27 | if (!open) { 28 | return null 29 | } 30 | 31 | function close() { 32 | setState(false) 33 | window.localStorage.setItem(key, true) 34 | } 35 | 36 | return ( 37 |
38 |
39 |

Black Lives Matter.

40 | 41 | Help end police violence in America → 42 | 43 | 46 |
47 | 101 |
102 | ) 103 | } 104 | 105 | export default Toast 106 | -------------------------------------------------------------------------------- /components/ApiContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import api from '../lib/api' 3 | 4 | const Context = React.createContext(api) 5 | 6 | export function useAPI() { 7 | return React.useContext(Context) 8 | } 9 | 10 | export default Context 11 | -------------------------------------------------------------------------------- /components/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import firebase from '../lib/client' 3 | // IDEA: just read from firebase store at request time? 4 | import { client } from '../lib/api' 5 | 6 | export const Context = React.createContext(null) 7 | 8 | export function useAuth() { 9 | return React.useContext(Context) 10 | } 11 | 12 | function AuthContext(props) { 13 | const [user, setState] = React.useState(null) 14 | 15 | React.useEffect(() => { 16 | if (firebase) { 17 | firebase.auth().onAuthStateChanged(newUser => setState(newUser)) 18 | } 19 | }, []) 20 | 21 | React.useEffect(() => { 22 | if (user) { 23 | user.getIdToken().then(jwt => { 24 | client.defaults.headers['Authorization'] = jwt ? `Bearer ${jwt}` : undefined 25 | }) 26 | } else { 27 | delete client.defaults.headers['Authorization'] 28 | } 29 | }, [user]) 30 | 31 | return {props.children} 32 | } 33 | 34 | export default AuthContext 35 | -------------------------------------------------------------------------------- /components/BackgroundSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import ImagePicker from './ImagePicker' 4 | import ColorPicker from './ColorPicker' 5 | import Button from './Button' 6 | import Popout, { managePopout } from './Popout' 7 | import { COLORS, DEFAULT_BG_COLOR } from '../lib/constants' 8 | import { stringifyColor } from '../lib/util' 9 | 10 | function validateColor(str) { 11 | if (/#\d{3,6}|rgba{0,1}\(.*?\)/gi.test(str) || /\w+/gi.test(str)) { 12 | return str 13 | } 14 | } 15 | 16 | class BackgroundSelect extends React.PureComponent { 17 | selectTab = name => { 18 | if (this.props.mode !== name) { 19 | this.props.onChange({ backgroundMode: name }) 20 | } 21 | } 22 | 23 | handlePickColor = color => this.props.onChange({ backgroundColor: stringifyColor(color) }) 24 | 25 | render() { 26 | const { 27 | color, 28 | mode, 29 | image, 30 | onChange, 31 | isVisible, 32 | toggleVisibility, 33 | carbonRef, 34 | updateHighlights, 35 | } = this.props 36 | 37 | const background = validateColor(color) ? color : DEFAULT_BG_COLOR 38 | 39 | const aspectRatio = carbonRef ? carbonRef.clientWidth / carbonRef.clientHeight : 1 40 | 41 | return ( 42 |
43 | 57 | 58 | 92 | 147 |
148 | ) 149 | } 150 | } 151 | 152 | export default managePopout(BackgroundSelect) 153 | -------------------------------------------------------------------------------- /components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VisuallyHidden from '@reach/visually-hidden' 3 | 4 | import { COLORS } from '../lib/constants' 5 | 6 | const Button = ({ 7 | id, 8 | onClick = () => {}, 9 | background = COLORS.BLACK, 10 | color = COLORS.SECONDARY, 11 | textColor, 12 | hoverBackground = COLORS.HOVER, 13 | hoverColor, 14 | disabled, 15 | selected, 16 | children, 17 | border, 18 | center, 19 | large, 20 | flex = 1, 21 | padding = 0, 22 | margin = 0, 23 | title, 24 | Component = 'button', 25 | display, 26 | ...props 27 | }) => ( 28 | 29 | {title && {title}} 30 | {children} 31 | 62 | 63 | ) 64 | 65 | export default Button 66 | -------------------------------------------------------------------------------- /components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SketchPicker from 'react-color/lib/Sketch' 3 | 4 | import { COLORS } from '../lib/constants' 5 | import { stringifyColor } from '../lib/util' 6 | 7 | const pickerStyle = { 8 | backgroundColor: COLORS.BLACK, 9 | padding: '8px 8px 0', 10 | margin: '0 auto 1px', 11 | } 12 | 13 | export default function ColorPicker(props) { 14 | const [color, setColor] = React.useState(props.color) 15 | const { onChange = () => {}, presets, style, disableAlpha } = props 16 | 17 | return ( 18 | 19 | 27 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /components/ConfirmButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from './Button' 3 | 4 | export default function ConfirmButton(props) { 5 | const [confirmed, setConfirmed] = React.useState(false) 6 | return ( 7 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/CopyMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRouter } from 'next/router' 3 | import { useCopyTextHandler, useAsyncCallback, useKeyboardListener } from 'actionsack' 4 | import morph from 'morphmorph' 5 | 6 | import { COLORS } from '../lib/constants' 7 | import Button from './Button' 8 | import Popout, { managePopout } from './Popout' 9 | import CopySVG from './svg/Copy' 10 | 11 | const toIFrame = (url, width, height) => 12 | ` 19 | ` 20 | 21 | const toURL = url => `${location.origin}${url}` 22 | // Medium does not handle asterisks correctly - https://github.com/carbon-app/carbon/issues/1067 23 | const replaceAsterisks = string => string.replace(/\*/g, '%2A') 24 | const toEncodedURL = morph.compose(encodeURI, replaceAsterisks, toURL) 25 | 26 | function CopyButton(props) { 27 | return ( 28 | 93 | 94 | 115 | 140 | 141 | ) 142 | } 143 | 144 | export default managePopout(React.memo(CopyMenu)) 145 | -------------------------------------------------------------------------------- /components/EditorContainer.js: -------------------------------------------------------------------------------- 1 | // Theirs 2 | import React from 'react' 3 | import Router from 'next/router' 4 | 5 | import Editor from './Editor' 6 | import Toasts from './Toasts' 7 | import { useAuth } from './AuthContext' 8 | 9 | import { THEMES } from '../lib/constants' 10 | import { updateRouteState } from '../lib/routing' 11 | import { getThemes, saveThemes, clearSettings, saveSettings } from '../lib/util' 12 | 13 | function onReset() { 14 | clearSettings() 15 | updateRouteState(Router, {}) 16 | 17 | if (window.navigator && navigator.serviceWorker) { 18 | navigator.serviceWorker.getRegistrations().then(registrations => { 19 | registrations.forEach(registration => { 20 | registration.unregister() 21 | }) 22 | }) 23 | } 24 | } 25 | 26 | function toastsReducer(curr, action) { 27 | switch (action.type) { 28 | case 'ADD': { 29 | return curr.concat(action.toast) 30 | } 31 | case 'SET': { 32 | return action.toasts 33 | } 34 | } 35 | throw new Error('Unsupported action') 36 | } 37 | 38 | function EditorContainer(props) { 39 | const [themes, updateThemes] = React.useState(THEMES) 40 | const user = useAuth() 41 | 42 | React.useEffect(() => { 43 | const storedThemes = getThemes(localStorage) || [] 44 | if (storedThemes) { 45 | updateThemes(currentThemes => [...storedThemes, ...currentThemes]) 46 | } 47 | }, []) 48 | 49 | React.useEffect(() => { 50 | saveThemes(themes.filter(({ custom }) => custom)) 51 | }, [themes]) 52 | 53 | // XXX use context 54 | const [snippet, setSnippet] = React.useState(props.snippet || null) 55 | // TODO update this reducer to only take one action 56 | const [toasts, setToasts] = React.useReducer(toastsReducer, []) 57 | 58 | const snippetId = snippet && snippet.id 59 | React.useEffect(() => { 60 | const snippetPath = '/' + (snippetId || '') 61 | if (snippetPath === props.router.asPath) { 62 | return 63 | } 64 | 65 | // Reloads only if the snipped.id is different from before. Otherwise returns from above. 66 | props.router.push( 67 | { 68 | pathname: '/[id]', 69 | query: { id: snippetId }, 70 | }, 71 | snippetPath, 72 | { 73 | shallow: true, 74 | scroll: false 75 | } 76 | ) 77 | }, [snippetId, props.router]) 78 | 79 | function onEditorUpdate(state) { 80 | if (user) { 81 | return 82 | } 83 | updateRouteState(props.router, state) 84 | saveSettings(state) 85 | } 86 | 87 | return ( 88 | <> 89 | 90 | 100 | 101 | ) 102 | } 103 | 104 | export default EditorContainer 105 | -------------------------------------------------------------------------------- /components/ExportMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useKeyboardListener, useAsyncCallback } from 'actionsack' 3 | 4 | import { COLORS, EXPORT_SIZES } from '../lib/constants' 5 | import Button from './Button' 6 | import Input from './Input' 7 | import Popout, { managePopout } from './Popout' 8 | 9 | import { Down as ArrowDown } from './svg/Arrows' 10 | 11 | const popoutStyle = { width: '256px', right: 0 } 12 | 13 | function preventDefault(fn) { 14 | return e => { 15 | e.preventDefault() 16 | return fn(e) 17 | } 18 | } 19 | 20 | function ExportMenu({ onChange, exportSize, isVisible, toggleVisibility, exportImage: exp }) { 21 | const input = React.useRef() 22 | 23 | const [exportImage, { loading }] = useAsyncCallback(exp) 24 | 25 | const handleExportSizeChange = selectedSize => () => onChange('exportSize', selectedSize) 26 | 27 | const handleExport = format => () => 28 | exportImage(format, { filename: input.current && input.current.value }) 29 | 30 | useKeyboardListener('⌘-⇧-e', preventDefault(handleExport('blob'))) 31 | useKeyboardListener('⌘-⇧-s', preventDefault(handleExport('svg'))) 32 | 33 | return ( 34 |
35 |
36 | 48 | 67 |
68 | 133 | 179 |
180 | ) 181 | } 182 | 183 | export default managePopout(React.memo(ExportMenu)) 184 | -------------------------------------------------------------------------------- /components/FontFace.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function FontFace(config) { 4 | return ( 5 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/FontSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ListSetting from './ListSetting' 3 | import ReferralLink from './ReferralLink' 4 | import { FONTS } from '../lib/constants' 5 | import { fileToDataURL as blobToUrl } from '../lib/util' 6 | 7 | const EXTENSIONS = ['.otf', '.ttf', '.woff'] 8 | 9 | const Font = ({ id, name, link }) => ( 10 | 11 | 12 | {name} 13 | 14 | {link && ( 15 | 16 | 17 | Purchase 18 | 19 | 20 | )} 21 | 22 | ) 23 | 24 | function FontSelect(props) { 25 | const inputEl = React.useRef(null) 26 | 27 | function onChange(id) { 28 | if (id === 'upload') { 29 | inputEl.current.click() 30 | } else { 31 | props.onChange(id) 32 | } 33 | } 34 | 35 | async function onFiles(e) { 36 | const { files } = e.target 37 | 38 | const name = files[0].name.split('.')[0] 39 | const url = await blobToUrl(files[0]) 40 | 41 | props.onUpload(name, url) 42 | } 43 | 44 | return ( 45 |
46 | 52 | {Font} 53 | 54 | 62 |
63 | ) 64 | } 65 | 66 | export default FontSelect 67 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | 4 | import { COLORS } from '../lib/constants' 5 | 6 | const Footer = () => ( 7 |
8 | 28 | 29 |
30 | created by{' '} 31 | 32 | @carbon_app 33 | {' '} 34 | ¬ 35 |
36 | 84 |
85 | ) 86 | 87 | export default Footer 88 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Logo from './svg/Logo' 3 | 4 | const Header = ({ enableHeroText }) => ( 5 |
6 |
7 | 8 | 9 | 10 | {enableHeroText ? ( 11 |

12 | Create and share beautiful images of your source code. 13 |
14 | Start typing or drop a file into the text area to get started. 15 |

16 | ) : null} 17 |
18 | 45 |
46 | ) 47 | 48 | export default Header 49 | -------------------------------------------------------------------------------- /components/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { COLORS } from '../lib/constants' 4 | 5 | const Input = React.forwardRef( 6 | ( 7 | { 8 | color = COLORS.SECONDARY, 9 | align = 'right', 10 | width = '100%', 11 | fontSize = '12px', 12 | label, 13 | ...props 14 | }, 15 | ref 16 | ) => ( 17 | 18 | {label && } 19 | 20 | 55 | 56 | ) 57 | ) 58 | 59 | export default Input 60 | -------------------------------------------------------------------------------- /components/ListSetting.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Checkmark from './svg/Checkmark' 4 | import { COLORS } from '../lib/constants' 5 | import { toggle } from '../lib/util' 6 | 7 | class ListSetting extends React.Component { 8 | static defaultProps = { 9 | onOpen: () => {}, 10 | onClose: () => {}, 11 | } 12 | 13 | state = { isVisible: false } 14 | 15 | select = id => { 16 | if (this.props.selected !== id) { 17 | this.props.onChange(id) 18 | } 19 | } 20 | 21 | toggle = () => { 22 | const handler = this.state.isVisible ? this.props.onClose : this.props.onOpen 23 | handler() 24 | this.setState(toggle('isVisible')) 25 | } 26 | 27 | renderListItems() { 28 | return this.props.items.map(item => ( 29 |
36 | {this.props.children(item, this.props.selected)} 37 | {this.props.selected === item.id ? : null} 38 | 58 |
59 | )) 60 | } 61 | 62 | render() { 63 | const { items, selected, title, children } = this.props 64 | const { isVisible } = this.state 65 | 66 | const selectedItem = items.filter(item => item.id === selected)[0] || {} 67 | 68 | return ( 69 |
70 |
76 | {title} 77 | {children(selectedItem)} 78 |
79 |
{this.renderListItems()}
80 | 101 |
102 | ) 103 | } 104 | } 105 | 106 | export default ListSetting 107 | -------------------------------------------------------------------------------- /components/LoginButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from 'next/link' 3 | import firebase, { logout, loginGitHub } from '../lib/client' 4 | 5 | import Button from './Button' 6 | import Popout, { managePopout } from './Popout' 7 | import { useAuth } from './AuthContext' 8 | 9 | function Drawer(props) { 10 | return ( 11 | 45 | ) 46 | } 47 | 48 | function LoginButton({ isVisible, toggleVisibility }) { 49 | const user = useAuth() 50 | 51 | if (!firebase) { 52 | return null 53 | } 54 | 55 | return ( 56 |
57 | 79 | 80 | 102 |
103 | ) 104 | } 105 | 106 | export default managePopout(LoginButton) 107 | -------------------------------------------------------------------------------- /components/MenuButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Button from './Button' 4 | import { COLORS } from '../lib/constants' 5 | import * as Arrows from './svg/Arrows' 6 | 7 | const MenuButton = React.memo(({ name, select, selected, noArrows }) => { 8 | return ( 9 |
10 | 22 | 43 |
44 | ) 45 | }) 46 | 47 | export default MenuButton 48 | -------------------------------------------------------------------------------- /components/Meta.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import { THEMES, THEMES_HASH, COLORS } from '../lib/constants' 4 | import Reset from './style/Reset' 5 | import Font from './style/Font' 6 | import Typography from './style/Typography' 7 | 8 | const CODEMIRROR_VERSION = '5.65.5' 9 | 10 | export const HIGHLIGHTS_ONLY = ['shades-of-purple', 'vscode', 'a11y-dark'] 11 | const LOCAL_STYLESHEETS = ['one-light', 'one-dark', 'verminal', 'night-owl', 'nord', 'synthwave-84'] 12 | const CDN_STYLESHEETS = THEMES.filter( 13 | t => LOCAL_STYLESHEETS.indexOf(t.id) < 0 && HIGHLIGHTS_ONLY.indexOf(t.id) < 0 14 | ) 15 | 16 | export function Link({ href }) { 17 | return ( 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export const StylesheetLink = ({ theme }) => { 26 | let href 27 | if (LOCAL_STYLESHEETS.indexOf(theme) > -1) { 28 | href = `/static/themes/${theme}.min.css` 29 | } else { 30 | const themeDef = THEMES_HASH[theme] 31 | href = `//cdnjs.cloudflare.com/ajax/libs/codemirror/${CODEMIRROR_VERSION}/theme/${ 32 | themeDef && (themeDef.link || themeDef.id) 33 | }.min.css` 34 | } 35 | 36 | return 37 | } 38 | 39 | export const CodeMirrorLink = () => ( 40 | 43 | ) 44 | 45 | const title = 'Carbon' 46 | const description = 47 | 'Carbon is the easiest way to create and share beautiful images of your source code.' 48 | export const MetaTags = React.memo(() => ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {title} | Create and share beautiful images of your source code 65 | 66 | 67 | 68 | 69 | )) 70 | 71 | export const MetaLinks = React.memo(() => { 72 | return ( 73 | 74 | 77 | 78 | {LOCAL_STYLESHEETS.map(id => ( 79 | 80 | ))} 81 | {CDN_STYLESHEETS.map(themeDef => { 82 | const href = `//cdnjs.cloudflare.com/ajax/libs/codemirror/${CODEMIRROR_VERSION}/theme/${ 83 | themeDef && (themeDef.link || themeDef.id) 84 | }.min.css` 85 | return 86 | })} 87 | 88 | ) 89 | }) 90 | 91 | export default React.memo(function Meta() { 92 | return ( 93 | 94 | 95 | 96 | 97 | 98 | 99 | ) 100 | }) 101 | -------------------------------------------------------------------------------- /components/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Overlay = props => ( 4 |
5 | {props.isOver ?
{props.title}
: null} 6 | {props.children} 7 | 28 |
29 | ) 30 | 31 | export default Overlay 32 | -------------------------------------------------------------------------------- /components/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AuthContext from './AuthContext' 3 | import Meta from './Meta' 4 | import Header from './Header' 5 | import Footer from './Footer' 6 | import Announcement from './Announcement' 7 | import LoginButton from './LoginButton' 8 | 9 | const COLUMN = ` 10 | display: flex; 11 | justify-content: center; 12 | flex-direction: column; 13 | align-items: center; 14 | ` 15 | class Page extends React.Component { 16 | render() { 17 | const { children, enableHeroText, flex } = this.props 18 | return ( 19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
{children}
28 | 29 | 30 |
31 | 32 | 57 |
58 | ) 59 | } 60 | } 61 | 62 | export default Page 63 | -------------------------------------------------------------------------------- /components/PhotoCredit.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function PhotoCredit({ photographer }) { 4 | return ( 5 |
6 | Photo by{' '} 7 | 8 | {photographer.name} 9 | 10 | 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/Popout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import enhanceWithClickOutside from 'react-click-outside' 3 | 4 | import WindowPointer from './WindowPointer' 5 | import { COLORS } from '../lib/constants' 6 | import { toggle } from '../lib/util' 7 | 8 | export const managePopout = WrappedComponent => { 9 | class PopoutManager extends React.Component { 10 | state = { 11 | isVisible: false, 12 | } 13 | 14 | toggleVisibility = () => this.setState(toggle('isVisible')) 15 | 16 | handleClickOutside = () => this.setState({ isVisible: false }) 17 | 18 | handleKeyDown = e => { 19 | if (e.key === 'Escape') { 20 | this.handleClickOutside() 21 | } 22 | } 23 | 24 | componentDidMount() { 25 | document.addEventListener('keydown', this.handleKeyDown) 26 | } 27 | 28 | componentWillUnmount() { 29 | document.removeEventListener('keydown', this.handleKeyDown) 30 | } 31 | 32 | render() { 33 | return ( 34 | 39 | ) 40 | } 41 | } 42 | 43 | return enhanceWithClickOutside(PopoutManager) 44 | } 45 | 46 | class Popout extends React.PureComponent { 47 | static defaultProps = { 48 | borderColor: COLORS.SECONDARY, 49 | style: {}, 50 | } 51 | 52 | render() { 53 | const { id, children, borderColor, style, hidden, pointerLeft, pointerRight } = this.props 54 | 55 | if (hidden) { 56 | return null 57 | } 58 | 59 | return ( 60 |
61 | 62 | {children} 63 | 76 |
77 | ) 78 | } 79 | } 80 | 81 | export default Popout 82 | -------------------------------------------------------------------------------- /components/RandomImage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAsyncCallback } from 'actionsack' 3 | 4 | import { Spinner } from './Spinner' 5 | import { useAPI } from './ApiContext' 6 | import PhotoCredit from './PhotoCredit' 7 | 8 | function RandomImage(props) { 9 | const cacheRef = React.useRef([]) 10 | const [cacheIndex, updateIndex] = React.useState(0) 11 | const api = useAPI() 12 | 13 | const [selectImage, { loading: selecting }] = useAsyncCallback(() => { 14 | const image = cacheRef.current[cacheIndex] 15 | 16 | return api.unsplash.download(image.id).then(data => props.onChange({ ...image, ...data })) 17 | }) 18 | 19 | const [updateCache, { loading: updating, error, data: imgs }] = useAsyncCallback( 20 | api.unsplash.random 21 | ) 22 | 23 | const needsFetch = !error && !updating && (!imgs || cacheIndex > cacheRef.current.length - 2) 24 | 25 | React.useEffect(() => { 26 | if (needsFetch) { 27 | updateCache() 28 | } 29 | }, [needsFetch, updateCache]) 30 | 31 | React.useEffect(() => { 32 | if (imgs) { 33 | cacheRef.current.push(...imgs) 34 | } 35 | }, [imgs]) 36 | 37 | const loading = updating || selecting 38 | 39 | const cache = cacheRef.current 40 | const photographer = cache[cacheIndex] && cache[cacheIndex].photographer 41 | const bgImage = cache[cacheIndex] && cache[cacheIndex].dataURL 42 | return ( 43 |
44 |
45 | 48 | 51 |
52 |
{loading && }
53 | {photographer && } 54 | 89 |
90 | ) 91 | } 92 | 93 | export default RandomImage 94 | -------------------------------------------------------------------------------- /components/ReferralLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { COLORS } from '../lib/constants' 4 | 5 | export default function ReferralLink(props) { 6 | return ( 7 | 8 | {props.children} 9 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/SelectionEditor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useKeyboardListener } from 'actionsack' 3 | import Popout from './Popout' 4 | import Button from './Button' 5 | import ColorPicker from './ColorPicker' 6 | import { COLORS } from '../lib/constants' 7 | 8 | function ModifierButton(props) { 9 | return ( 10 | 21 | ) 22 | } 23 | 24 | function reducer(state, action) { 25 | switch (action.type) { 26 | case 'BOLD': { 27 | return { 28 | ...state, 29 | bold: !state.bold, 30 | } 31 | } 32 | case 'ITALICS': { 33 | return { 34 | ...state, 35 | italics: !state.italics, 36 | } 37 | } 38 | case 'UNDERLINE': { 39 | return { 40 | ...state, 41 | underline: Number(state.underline + 1) % 3, 42 | } 43 | } 44 | case 'COLOR': { 45 | return { 46 | ...state, 47 | color: action.color, 48 | } 49 | } 50 | } 51 | throw new Error('Invalid action') 52 | } 53 | 54 | function SelectionEditor({ onChange }) { 55 | const [open, setOpen] = React.useState(false) 56 | 57 | useKeyboardListener('Escape', () => setOpen(false)) 58 | 59 | const [state, dispatch] = React.useReducer(reducer, { 60 | bold: null, 61 | italics: null, 62 | underline: null, 63 | color: null, 64 | }) 65 | 66 | React.useEffect(() => { 67 | onChange(state) 68 | }, [onChange, state]) 69 | 70 | return ( 71 |
72 |
73 |
74 | dispatch({ type: 'BOLD' })}> 75 | B 76 | 77 | dispatch({ type: 'ITALICS' })}> 78 | I 79 | 80 | dispatch({ type: 'UNDERLINE' })} 83 | color={state.underline === 2 ? COLORS.RED : undefined} 84 | > 85 | U 86 | 87 |
89 | 98 |
99 | 131 |
132 | ) 133 | } 134 | 135 | export default SelectionEditor 136 | -------------------------------------------------------------------------------- /components/ShareMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAsyncCallback, useOnline as useOnlineListener } from 'actionsack' 3 | 4 | import { useAPI } from './ApiContext' 5 | import { COLORS } from '../lib/constants' 6 | import Button from './Button' 7 | import Popout, { managePopout } from './Popout' 8 | import { Down as ArrowDown } from './svg/Arrows' 9 | 10 | const popoutStyle = { width: '144px', right: 8 } 11 | 12 | function ShareMenu({ isVisible, toggleVisibility, tweet, imgur }) { 13 | const api = useAPI() 14 | const online = useOnlineListener() 15 | 16 | const [onClickTweet, { loading: tweeting }] = useAsyncCallback(tweet) 17 | const [onClickImgur, { loading: imguring }] = useAsyncCallback(imgur) 18 | 19 | if (!api || !api.tweet) { 20 | return null 21 | } 22 | 23 | if (!online) { 24 | return null 25 | } 26 | 27 | return ( 28 |
29 |
30 | 42 | 61 |
62 | 69 | 83 |
84 | ) 85 | } 86 | 87 | export default managePopout(React.memo(ShareMenu)) 88 | -------------------------------------------------------------------------------- /components/Slider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { COLORS } from '../lib/constants' 4 | 5 | class Slider extends React.Component { 6 | static defaultProps = { 7 | onMouseDown: () => {}, 8 | onMouseUp: () => {}, 9 | unit: 'px', 10 | } 11 | 12 | handleChange = e => { 13 | this.props.onChange(`${e.target.value}${this.props.unit}`) 14 | } 15 | 16 | render() { 17 | const minValue = this.props.minValue || 0 18 | const maxValue = this.props.maxValue || 100 19 | const step = 'step' in this.props ? this.props.step : 1 20 | 21 | return ( 22 |
23 |
31 | 32 | 42 | 82 |
83 | ) 84 | } 85 | } 86 | 87 | export default Slider 88 | -------------------------------------------------------------------------------- /components/SnippetToolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useAsyncCallback, useOnline, useKeyboardListener } from 'actionsack' 3 | 4 | import Button from './Button' 5 | import Toolbar from './Toolbar' 6 | import Input from './Input' 7 | import ConfirmButton from './ConfirmButton' 8 | import Popout, { managePopout } from './Popout' 9 | import { Down as ArrowDown } from './svg/Arrows' 10 | import { useAuth } from './AuthContext' 11 | 12 | import { COLORS } from '../lib/constants' 13 | 14 | const popoutStyle = { width: '120px', right: -8, top: 40 } 15 | 16 | function DeleteButton(props) { 17 | const [onClick, { loading }] = useAsyncCallback(props.onClick) 18 | 19 | return ( 20 | 30 | {loading ? 'Deleting…' : 'Delete'} 31 | 32 | ) 33 | } 34 | 35 | function DuplicateButton(props) { 36 | const [onClick, { loading }] = useAsyncCallback(props.onClick) 37 | 38 | return ( 39 | 52 | ) 53 | } 54 | 55 | function SaveButton({ loading, onClick, sameUser }) { 56 | useKeyboardListener('⌥-s', e => { 57 | if (loading) { 58 | return 59 | } 60 | e.preventDefault() 61 | onClick() 62 | }) 63 | 64 | return ( 65 | 82 | ) 83 | } 84 | 85 | function SnippetToolbar({ toggleVisibility, isVisible, snippet, ...props }) { 86 | const user = useAuth() 87 | const online = useOnline() 88 | 89 | const [save, { loading }] = useAsyncCallback(() => { 90 | if (snippet) { 91 | return props.onUpdate() 92 | } else { 93 | return props.onCreate() 94 | } 95 | }) 96 | 97 | if (!online) return null 98 | if (!user) return null 99 | 100 | const sameUser = snippet && user.uid === snippet.userId 101 | 102 | return ( 103 | 113 | 118 |
119 | props.onChange('name', e.target.value)} 125 | /> 126 |
127 |
128 | {snippet && !sameUser ? ( 129 | 130 | ) : ( 131 | 132 | )} 133 | {sameUser && ( 134 | 152 | )} 153 |
154 | 161 |
162 | ) 163 | } 164 | 165 | export default managePopout(SnippetToolbar) 166 | -------------------------------------------------------------------------------- /components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function Spinner({ size = 24 }) { 4 | return ( 5 |
6 |
7 |
8 | 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /components/ThemeSelect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Toggle from './Toggle' 3 | import { None, BW, Sharp, Boxy } from './svg/WindowThemes' 4 | import { COLORS } from '../lib/constants' 5 | 6 | const WINDOW_THEMES_MAP = { none: None, sharp: Sharp, bw: BW, boxy: Boxy } 7 | 8 | class ThemeSelect extends React.Component { 9 | select = theme => { 10 | if (this.props.selected !== theme) { 11 | this.props.onChange('windowTheme', theme) 12 | } 13 | } 14 | 15 | renderThemes() { 16 | return Object.keys(WINDOW_THEMES_MAP).map(theme => { 17 | const Img = WINDOW_THEMES_MAP[theme] 18 | const checked = this.props.selected === theme 19 | return ( 20 |
29 | 30 | 51 |
52 | ) 53 | }) 54 | } 55 | 56 | render() { 57 | return ( 58 | <> 59 |
60 | this.props.onChange('windowControls', v)} 64 | /> 65 | {this.props.windowControls && ( 66 |
67 | {this.renderThemes()} 68 |
69 | )} 70 | 87 |
88 | 89 | ) 90 | } 91 | } 92 | 93 | export default ThemeSelect 94 | -------------------------------------------------------------------------------- /components/Themes/GlobalHighlights.js: -------------------------------------------------------------------------------- 1 | // Theirs 2 | import React from 'react' 3 | 4 | export default function GlobalHighlights({ highlights }) { 5 | return ( 6 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /components/Themes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import dynamic from 'next/dynamic' 3 | 4 | import GlobalHighlights from './GlobalHighlights' 5 | import Dropdown from '../Dropdown' 6 | import { managePopout } from '../Popout' 7 | import ReferralLink from '../ReferralLink' 8 | import ThemeIcon from '../svg/Theme' 9 | import RemoveIcon from '../svg/Remove' 10 | import { COLORS } from '../../lib/constants' 11 | 12 | const ThemeCreate = dynamic(() => import('./ThemeCreate'), { 13 | loading: () => null, 14 | }) 15 | 16 | const ThemeItem = ({ children, item, isSelected, remove }) => ( 17 |
18 | {children} 19 | {item.referral && ( 20 |
21 | Purchase 22 |
23 | )} 24 | {item.custom && !isSelected && ( 25 |
{ 30 | e.stopPropagation() 31 | remove(item.id) 32 | }} 33 | > 34 | 35 |
36 | )} 37 | 52 |
53 | ) 54 | 55 | const themeIcon = 56 | 57 | const getCustomName = themes => 58 | `Custom Theme ${themes.filter(({ name }) => name.startsWith('Custom Theme')).length + 1}` 59 | 60 | class Themes extends React.PureComponent { 61 | state = { 62 | name: '', 63 | } 64 | 65 | dropdown = React.createRef() 66 | 67 | static getDerivedStateFromProps(props) { 68 | if (!props.isVisible) { 69 | return { 70 | name: getCustomName(props.themes), 71 | } 72 | } 73 | return null 74 | } 75 | 76 | handleThemeSelected = theme => { 77 | if (theme) { 78 | const { toggleVisibility, update } = this.props 79 | if (theme.id === 'create') { 80 | toggleVisibility() 81 | this.dropdown.current.closeMenu() 82 | } else { 83 | update(theme.id) 84 | } 85 | } 86 | } 87 | 88 | create = theme => { 89 | this.props.toggleVisibility() 90 | this.props.create(theme) 91 | } 92 | 93 | itemWrapper = props => 94 | 95 | render() { 96 | const { themes, theme, isVisible, toggleVisibility } = this.props 97 | 98 | const highlights = { ...theme.highlights, ...this.props.highlights } 99 | 100 | const dropdownValue = isVisible ? { name: this.state.name } : theme 101 | 102 | const dropdownList = [ 103 | { 104 | id: 'create', 105 | name: 'Create +', 106 | }, 107 | ...themes, 108 | ] 109 | 110 | return ( 111 |
112 | 123 | {isVisible && ( 124 | this.setState({ name: e.target.value })} 132 | /> 133 | )} 134 | 135 | 142 |
143 | ) 144 | } 145 | } 146 | 147 | export default managePopout(Themes) 148 | -------------------------------------------------------------------------------- /components/Toasts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Toast(props) { 4 | const [display, on] = React.useState(true) 5 | 6 | function off() { 7 | return on(false) 8 | } 9 | 10 | React.useEffect(() => { 11 | if (props.timeout) { 12 | const to = setTimeout(off, props.timeout) 13 | return () => clearTimeout(to) 14 | } 15 | }, [props.timeout]) 16 | 17 | return ( 18 |
19 |
20 | {props.children} 21 | {props.closable && ( 22 | 25 | )} 26 |
27 | 89 |
90 | ) 91 | } 92 | 93 | function ToastContainer(props) { 94 | return ( 95 |
96 | {props.toasts 97 | ? props.toasts 98 | .slice() 99 | .reverse() 100 | .map(toast => ) 101 | : null} 102 | 112 |
113 | ) 114 | } 115 | 116 | export default ToastContainer 117 | -------------------------------------------------------------------------------- /components/Toggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Checkmark from './svg/Checkmark' 4 | import { COLORS } from '../lib/constants' 5 | 6 | class Toggle extends React.PureComponent { 7 | static defaultProps = { 8 | className: '', 9 | } 10 | 11 | toggle = () => this.props.onChange(!this.props.enabled) 12 | 13 | render() { 14 | return ( 15 |
16 | 17 | 23 | {this.props.enabled ? :
} 24 | 63 |
64 | ) 65 | } 66 | } 67 | 68 | export default Toggle 69 | -------------------------------------------------------------------------------- /components/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Toolbar = props => ( 4 |
5 | {props.children} 6 | 35 |
36 | ) 37 | 38 | export default Toolbar 39 | -------------------------------------------------------------------------------- /components/WidthHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DEFAULT_WIDTHS, COLORS } from '../lib/constants' 3 | 4 | const { minWidth, maxWidth } = DEFAULT_WIDTHS 5 | 6 | function clamp(value, min, max) { 7 | if (value < min) { 8 | return min 9 | } 10 | if (value > max) { 11 | return max 12 | } 13 | return value 14 | } 15 | 16 | export default function WidthHandler({ 17 | innerRef, 18 | onChange, 19 | onChangeComplete, 20 | paddingHorizontal, 21 | paddingVertical, 22 | }) { 23 | const startX = React.useRef(null) 24 | const startWidth = React.useRef(null) 25 | 26 | React.useEffect(() => { 27 | function handleMouseMove(e) { 28 | if (!startX.current) return 29 | 30 | const delta = e.pageX - startX.current // leftOrRight === 'left' ? startX - e.pageX : (startX - e.pageX) * -1 31 | const calculated = startWidth.current + delta * window.devicePixelRatio 32 | const newWidth = clamp(calculated, minWidth, maxWidth) 33 | 34 | onChange(newWidth) 35 | } 36 | 37 | window.addEventListener('mousemove', handleMouseMove) 38 | return () => window.removeEventListener('mousemove', handleMouseMove) 39 | }, [innerRef, onChange]) 40 | 41 | React.useEffect(() => { 42 | function handleMouseUp() { 43 | startX.current = null 44 | onChangeComplete() 45 | } 46 | window.addEventListener('mouseup', handleMouseUp) 47 | return () => window.removeEventListener('mouseup', handleMouseUp) 48 | }, [onChangeComplete]) 49 | 50 | return ( 51 | // eslint-disable-next-line 52 |
{ 55 | startX.current = e.pageX 56 | startWidth.current = innerRef.current.clientWidth 57 | }} 58 | role="separator" 59 | aria-orientation="vertical" 60 | aria-valuemin={minWidth} 61 | aria-valuemax={maxWidth} 62 | > 63 | 81 |
82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /components/WindowControls.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useCopyTextHandler } from 'actionsack' 3 | 4 | import { COLORS } from '../lib/constants' 5 | import { Controls, ControlsBW, ControlsBoxy } from './svg/Controls' 6 | import CopySVG from './svg/Copy' 7 | import CheckMark from './svg/Checkmark' 8 | 9 | const size = 24 10 | 11 | const CopyButton = React.memo(function CopyButton({ text }) { 12 | const { onClick, copied } = useCopyTextHandler(text) 13 | 14 | return ( 15 | 36 | ) 37 | }) 38 | 39 | const WINDOW_THEMES_MAP = { bw: , boxy: } 40 | 41 | export function TitleBar({ light, value, onChange }) { 42 | return ( 43 |
44 | onChange(e.target.value)} 50 | /> 51 | 78 |
79 | ) 80 | } 81 | 82 | export default function WindowControls({ 83 | theme, 84 | copyable, 85 | code, 86 | light, 87 | titleBar, 88 | onTitleBarChange, 89 | }) { 90 | return ( 91 |
92 | {WINDOW_THEMES_MAP[theme] || } 93 | 94 | {copyable && ( 95 |
96 | 97 |
98 | )} 99 | 119 |
120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /components/WindowPointer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function WindowPointer({ fromLeft, fromRight, color = '#fff' }) { 4 | return ( 5 |
6 |
7 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/hooks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function userTiming({ category, status, value }) { 4 | try { 5 | window.gtag('event', status, { 6 | event_category: 'Performance', 7 | event_label: category, 8 | value, 9 | }) 10 | } catch (err) { 11 | // pass 12 | } 13 | } 14 | 15 | export function usePerformanceMeasurement() { 16 | React.useEffect(() => { 17 | try { 18 | if (window.performance && window.performance.getEntriesByType) { 19 | window.performance.getEntriesByType('paint').forEach(entry => { 20 | userTiming({ 21 | category: 'paint', 22 | status: entry.name, 23 | value: entry.startTime, 24 | }) 25 | }) 26 | const navigationTiming = window.performance.getEntriesByType('navigation') 27 | ? window.performance.getEntriesByType('navigation')[0] 28 | : null 29 | if (navigationTiming) { 30 | userTiming({ 31 | category: 'paint', 32 | status: 'time to first byte', 33 | value: navigationTiming.responseEnd - navigationTiming.requestStart, 34 | }) 35 | } 36 | 37 | const javascriptFiles = performance.getEntries().filter(resource => { 38 | return resource.name.startsWith(`${location.origin}/_next/static`) 39 | }) 40 | 41 | /* 42 | * Tracks total number of javascript used, 43 | * helps in tracking the effect of granular chunks work 44 | */ 45 | userTiming({ 46 | category: 'javascript', 47 | status: 'script count', 48 | value: javascriptFiles.length, 49 | }) 50 | 51 | /* 52 | * Tracks total size of javascript used, 53 | * helps in tracking the effect of modern/nomodern work 54 | */ 55 | userTiming({ 56 | category: 'javascript', 57 | status: 'script size', 58 | value: javascriptFiles.reduce((sum, script) => script.encodedBodySize + sum, 0), 59 | }) 60 | } 61 | } catch (error) { 62 | console.error(error) 63 | } 64 | }, []) 65 | } 66 | -------------------------------------------------------------------------------- /components/style/Font.js: -------------------------------------------------------------------------------- 1 | /* 2 | * See https://developers.google.com/web/updates/2016/02/font-display and 3 | * https://css-tricks.com/font-display-masses/#article-header-id-2 4 | * for `font-display` information 5 | */ 6 | import React from 'react' 7 | 8 | export default function Font() { 9 | return ( 10 | 134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /components/style/Reset.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { COLORS } from '../../lib/constants' 3 | 4 | export default function Reset() { 5 | return ( 6 | 231 | ) 232 | } 233 | -------------------------------------------------------------------------------- /components/style/Typography.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Typography() { 4 | return ( 5 | 116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /components/svg/Arrows.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Up = ({ color = 'white' }) => ( 4 | 5 | 6 | 7 | ) 8 | 9 | const Down = ({ color = 'white' }) => ( 10 | 11 | 12 | 13 | ) 14 | 15 | const Right = ({ color = 'white' }) => ( 16 | 17 | 18 | 19 | ) 20 | 21 | export { Up, Down, Right } 22 | -------------------------------------------------------------------------------- /components/svg/Checkmark.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Checkmark({ width = 18, height = 18, color = '#FFFFFF' }) { 4 | return ( 5 | 12 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/svg/Controls.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Controls = () => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | 13 | export const ControlsBW = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | 23 | export const ControlsBoxy = () => ( 24 | 25 | 26 | 30 | 31 | 32 | 33 | ) 34 | -------------------------------------------------------------------------------- /components/svg/Copy.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SVG_RATIO = 0.81 4 | 5 | const Copy = ({ size, color }) => { 6 | const width = size * SVG_RATIO 7 | const height = size 8 | 9 | return ( 10 | 17 | 21 | 22 | ) 23 | } 24 | 25 | Copy.defaultProps = { 26 | size: 16, 27 | } 28 | 29 | export default Copy 30 | -------------------------------------------------------------------------------- /components/svg/Language.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Language() { 4 | return ( 5 | 6 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/svg/Remove.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Remove({ color = 'black', style }) { 4 | return ( 5 | 13 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/svg/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Settings() { 4 | return ( 5 | 6 | 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /components/svg/Theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Theme() { 4 | return ( 5 | 6 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /components/svg/WindowThemes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Sharp = () => ( 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 36 | 46 | 47 | 48 | 49 | 50 | ) 51 | 52 | export const BW = () => ( 53 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ) 83 | 84 | export const None = () => ( 85 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ) 114 | 115 | export const Boxy = () => ( 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 133 | 137 | 143 | 149 | 150 | 151 | 152 | ) 153 | -------------------------------------------------------------------------------- /cypress/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "https://carbon.now.sh/", 3 | "projectId": "p2tbx4", 4 | "video": false 5 | } 6 | -------------------------------------------------------------------------------- /cypress/integration/background-color.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | import { editorVisible } from '../support' 3 | 4 | // usually we can visit the page before each test 5 | // but these tests use the url, which means wasted page load 6 | // so instead visit the desired url in each test 7 | 8 | describe('background color', () => { 9 | const bgColor = '.bg-color-container .bg-color' 10 | const picker = '#bg-select-pickers' 11 | 12 | const openPicker = () => { 13 | cy.get(bgColor).click() 14 | return cy.get(picker).should('be.visible') 15 | } 16 | 17 | // clicking anywhere else closes it 18 | const closePicker = () => cy.get('body').click() 19 | 20 | it('opens BG color pick', () => { 21 | cy.visit('/') 22 | openPicker() 23 | cy.get('body').click(5, 5, { force: true }) 24 | cy.get(picker).should('not.exist') 25 | }) 26 | 27 | it('changes background color to dark red', () => { 28 | cy.visit('/') 29 | const darkRed = '#D0021B' 30 | const darkRedTile = `[title="${darkRed}"]` 31 | openPicker() 32 | cy.get(picker).find(darkRedTile).click() 33 | closePicker() 34 | 35 | // changing background color triggers url change 36 | cy.url().should('contain', '?bg=') 37 | 38 | // confirm color change 39 | cy.get('.container-bg .bg').should('have.css', 'background-color', 'rgb(208, 2, 27)') 40 | }) 41 | 42 | it('specifies color in url', () => { 43 | cy.visit('?bg=rgb(255,0,0)') 44 | editorVisible() 45 | cy.get('.container-bg .bg').should('have.css', 'background-color', 'rgb(255, 0, 0)') 46 | }) 47 | 48 | it('enters neon pink', () => { 49 | cy.visit('?bg=rgb(255,0,0)') 50 | editorVisible() 51 | 52 | const pink = 'ff00ff' 53 | openPicker().find(`input[value="FF0000"]`).clear().type(`${pink}{enter}`) 54 | closePicker() 55 | 56 | cy.url().should(url => expect(decodeURIComponent(url)).to.contain(`?bg=rgba(255,0,255,1)`)) 57 | cy.get('.container-bg .bg').should('have.css', 'background-color', 'rgb(255, 0, 255)') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /cypress/integration/basic.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | import { editorVisible } from '../support' 3 | describe('Basic', () => { 4 | it('Should open editor with the correct text encoding', () => { 5 | cy.visit( 6 | '/?code=%250A%252F*%2520Passing%2520Boolean%2520as%2520method%2520to%2520find%2520returns%2520the%250A%2520*%2520first%2520truthy%2520value%2520in%2520the%2520array!%250A%2520*%252F%250A%255Bfalse%252C%2520false%252C%2520%27%27%252C%2520undefined%252C%2520%27qwijo%27%252C%25200%255D.find(Boolean)%2520%252F%252F%2520%27qwijo%27' 7 | ) 8 | editorVisible() 9 | 10 | cy.contains( 11 | '.container', 12 | "/* Passing Boolean as method to find returns the * first truthy value in the array! */[false, false, '', undefined, 'qwijo', 0].find(Boolean) // 'qwijo'" 13 | ) 14 | }) 15 | 16 | it('Should open editor with the correct text even with bad URI component', () => { 17 | cy.visit('/?code=%25') 18 | editorVisible() 19 | 20 | cy.contains('.container', '%') 21 | }) 22 | 23 | it('Should clear editor state with Shift+Cmd+\\', () => { 24 | cy.visit('/?bg=red') 25 | 26 | cy.get('body').trigger('keydown', { key: '\\', metaKey: true, shiftKey: true }) 27 | 28 | cy.location().its('pathname').should('eq', '/') 29 | cy.get('.container-bg .bg').should('have.css', 'background-color', 'rgb(171, 184, 195)') 30 | }) 31 | 32 | it("Should contain id's for CLI integrations to use", () => { 33 | cy.get('#export-container').should('have.length', 1) 34 | cy.get('.export-container').should('have.length', 1) 35 | cy.get('#export-menu').should('have.length', 1) 36 | cy.get('#export-menu').click() 37 | cy.get('#export-png').should('have.length', 1) 38 | cy.get('#export-svg').should('have.length', 1) 39 | }) 40 | 41 | /* 42 | * This test should only be run locally since it actually downloads a file 43 | * for verification. 44 | */ 45 | it.skip('Should download a PNGs and SVGs', () => { 46 | cy.visit('/') 47 | editorVisible() 48 | 49 | cy.contains('span[type="button"]', 'Save Image').click() 50 | cy.get('#downshift-2-item-0').click() 51 | 52 | cy.wait(1000) 53 | 54 | cy.contains('span[type="button"]', 'Save Image').click() 55 | cy.get('#downshift-2-item-1').click() 56 | 57 | cy.wait(1000) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /cypress/integration/embed.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | describe('Embed', () => { 3 | it('Should render the Carbon editor but no toolbar', () => { 4 | cy.visit('/embed') 5 | 6 | cy.get('.export-container').should('be.visible') 7 | cy.get('.export-menu-container').should('not.exist') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/integration/gist.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy Cypress */ 2 | import { editorVisible } from '../support' 3 | 4 | describe('Gist', () => { 5 | const test = Cypress.env('CI') ? it.skip : it 6 | 7 | test('Should pull text from the first Gist file', () => { 8 | cy.visit('/3208813b324d82a9ebd197e4b1c3bae8') 9 | editorVisible() 10 | 11 | cy.contains('Y-Combinator implemented in JavaScript') 12 | cy.get('#downshift-input-JavaScript').should('have.value', 'JavaScript') 13 | }) 14 | 15 | const pages = ['/', '/embed/', '/82d742f4efad9757cc826d20f2a5e5af'] 16 | 17 | pages.forEach(page => { 18 | test(`${page} should not contain a query string in the url`, () => { 19 | cy.visit(page) 20 | 21 | cy.url().should('not.contain', '?') 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /cypress/integration/localStorage.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | import { editorVisible } from '../support' 3 | 4 | // usually we can visit the page before each test 5 | // but these tests use the url, which means wasted page load 6 | // so instead visit the desired url in each test 7 | 8 | describe('localStorage', () => { 9 | const themeDropdown = () => cy.get('.toolbar .dropdown-container').first() 10 | 11 | const pickTheme = (name = 'Blackboard') => 12 | themeDropdown() 13 | .click() 14 | .contains(name) 15 | .click() 16 | 17 | it.skip('is empty initially', () => { 18 | cy.visit('/') 19 | editorVisible() 20 | cy.window() 21 | .its('localStorage') 22 | .should('have.length', 0) 23 | }) 24 | 25 | it('saves on theme change', () => { 26 | cy.visit('/') 27 | editorVisible() 28 | pickTheme('Blackboard') 29 | themeDropdown() 30 | .click() 31 | .contains('Blackboard') 32 | 33 | cy.wait(1500) // URL updates are debounced 34 | 35 | cy.window() 36 | .its('localStorage.CARBON_STATE') 37 | .then(JSON.parse) 38 | .its('theme') 39 | .should('equal', 'blackboard') 40 | 41 | // visiting page again restores theme from localStorage 42 | cy.visit('/') 43 | pickTheme('Cobalt') 44 | cy.wait(1500) // URL updates are debounced 45 | 46 | cy.url().should('contain', 't=cobalt') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /cypress/integration/security.spec.js: -------------------------------------------------------------------------------- 1 | /* global cy */ 2 | import { editorVisible } from '../support' 3 | 4 | describe('security', () => { 5 | it('should not alert from bg query parameter', () => { 6 | const stub = cy.stub() 7 | cy.on('window:alert', stub) 8 | 9 | // https://github.com/carbon-app/carbon/issues/192 10 | cy.visit(`?bg=rgba(171, 184, 195, 1)