├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── .stylelintrc.js ├── CHANGELOG.md ├── README.md ├── assets └── css │ └── main.css ├── components ├── AppFooter.vue ├── AppMasthead.vue ├── AppNav.vue ├── AppNavLink.vue ├── BaseButton.vue ├── BaseInput.vue ├── ColorField.vue ├── ColorMode.vue ├── Gradient.vue ├── OptionControls.vue ├── OptionGroup.vue └── RangeField.vue ├── layouts ├── default.vue └── error.vue ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── _.vue ├── _content │ ├── about.md │ ├── background.md │ └── colophon.md ├── index.vue └── info.vue ├── static ├── favicon.ico ├── icon-1024.png └── icon.png ├── store └── index.js ├── tailwind.config.js ├── utils └── clipboard.js └── vercel.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", 3 | "access": "restricted", 4 | "baseBranch": "main", 5 | "changelog": [ 6 | "@changesets/changelog-github", 7 | { "repo": "stormwarning/polychroma" } 8 | ], 9 | "commit": false, 10 | "ignore": [], 11 | "linked": [], 12 | "updateInternalDependencies": "patch" 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{json,yml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@zazen/eslint-config', '@zazen/eslint-config/vue'], 3 | rules: { 4 | 'import/no-relative-parent-imports': 'error', 5 | 'import/order': 'error', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: npm 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | commit-message: 11 | prefix: 'Upgrade: ' 12 | versioning-strategy: increase-if-necessary 13 | 14 | - package-ecosystem: github-actions 15 | directory: / 16 | schedule: 17 | interval: weekly 18 | commit-message: 19 | prefix: 'Chore: ' 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all git history so that Changesets can 17 | # generate changelogs with the correct commits. 18 | fetch-depth: 0 19 | 20 | - name: Setup Node.js 12.x 21 | uses: actions/setup-node@master 22 | with: 23 | node-version: 12.x 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | # https://github.com/changesets/action 29 | - name: Create release PR 30 | id: changesets 31 | uses: changesets/action@master 32 | with: 33 | title: 'Chore: Publish release' 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | 13 | # Service worker 14 | sw.* 15 | 16 | .vercel -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@zazen/stylelint-config'], 3 | rules: { 4 | 'at-rule-no-unknown': [ 5 | true, 6 | { ignoreAtRules: ['include', 'mixin', 'screen', 'apply'] }, 7 | ], 8 | 'value-list-comma-newline-after': null, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ### 2.1.2 — 2022-01-07 4 | 5 | #### 🐛 Fixed 6 | 7 | - Fix mobile layout [#197](https://github.com/stormwarning/polychroma/pull/197) 8 | Layout on tablets no longer completely broken. 9 | - Replace Tachyons with Tailwind CSS [#162](https://github.com/stormwarning/polychroma/pull/162) 10 | Fixes issue with header overlay not being sticky. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # polychroma 2 | > Better gradients through colour spaces. 3 | 4 | Generate better CSS gradients using alternative colour spaces & Bezier interpolation. 5 | 6 | ### Credits 7 | Uses the amazing [chroma.js] library by @gka 8 | 9 | 10 | [chroma.js]: http://gka.github.io/chroma.js/ 11 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://rsms.me/inter/inter.css'); 2 | 3 | @import 'tailwindcss/base'; 4 | @import 'tailwindcss/components'; 5 | @import 'tailwindcss/utilities'; 6 | 7 | @viewport { 8 | width: device-width; 9 | } 10 | 11 | :root { 12 | --color-bg: #f5f5f5; 13 | --color-text: #717474; 14 | --color-heading: #16191b; 15 | 16 | --grey-1: #f5f5f5; 17 | --grey-3: #c2c3c4; 18 | --grey-4: #9b9e9d; 19 | --grey-6: #717474; 20 | --grey-7: #495057; 21 | --grey-8: #212529; 22 | --grey-9: #16191b; 23 | 24 | --blue-5: #339af0; 25 | --blue-6: #228be6; 26 | --blue-7: #1c7ed6; 27 | } 28 | 29 | code, 30 | .code { 31 | font-family: 'SF Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, 32 | monospace; 33 | } 34 | 35 | .mix-burn { 36 | mix-blend-mode: color-burn; 37 | } 38 | 39 | /** 40 | Fix tracking issue for right-aligned text. 41 | */ 42 | .tracking-mega { 43 | margin-right: -0.25em; 44 | } 45 | 46 | .nuxt-link-active { 47 | border-bottom: 0.125rem solid currentColor; 48 | } 49 | 50 | /** 51 | Page transition effects. 52 | 53 | `page` is the default transition name. 54 | @see https://nuxtjs.org/guide/routing#transitions 55 | */ 56 | .page-enter-active, 57 | .page-leave-active { 58 | transition: opacity 200ms ease-in; 59 | } 60 | .page-enter, 61 | .page-leave-to { 62 | opacity: 0; 63 | } 64 | -------------------------------------------------------------------------------- /components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 72 | 73 | 82 | -------------------------------------------------------------------------------- /components/AppMasthead.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | -------------------------------------------------------------------------------- /components/AppNav.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /components/AppNavLink.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /components/BaseButton.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /components/BaseInput.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 44 | -------------------------------------------------------------------------------- /components/ColorField.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 129 | -------------------------------------------------------------------------------- /components/ColorMode.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | -------------------------------------------------------------------------------- /components/Gradient.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 147 | 148 | 165 | -------------------------------------------------------------------------------- /components/OptionControls.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | 47 | 73 | -------------------------------------------------------------------------------- /components/OptionGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 43 | -------------------------------------------------------------------------------- /components/RangeField.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 64 | 65 | 187 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | 42 | 93 | -------------------------------------------------------------------------------- /layouts/error.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { version } from './package.json' 2 | 3 | function getVersion() { 4 | let string = JSON.stringify(version) || '' 5 | return string.replace(/"/g, '') 6 | } 7 | 8 | export default { 9 | /** Headers of the page. */ 10 | head: { 11 | htmlAttrs: { class: 'bg-grey-100 overscroll-none' }, 12 | bodyAttrs: { class: 'font-sans text-grey-800 antialiased' }, 13 | title: 'Polychroma — Better gradients through colour spaces.', 14 | meta: [ 15 | { charset: 'utf-8' }, 16 | { 17 | name: 'viewport', 18 | content: 'width=device-width, initial-scale=1', 19 | }, 20 | { 21 | hid: 'description', 22 | meta: 'description', 23 | content: 24 | 'Generate better CSS gradients using alternative colour spaces & Bezier interpolation.', 25 | }, 26 | 27 | { name: 'twitter:card', content: 'summary' }, 28 | { name: 'twitter:title', content: 'Polychroma' }, 29 | { 30 | name: 'twitter:description', 31 | content: 32 | 'Generate better CSS gradients using alternative colour spaces & Bezier interpolation.', 33 | }, 34 | { name: 'twitter:creator', content: '@stormwarning' }, 35 | { 36 | name: 'twitter:image:src', 37 | content: 'https://polychroma.app/icon.png', 38 | }, 39 | 40 | { property: 'og:title', content: 'Polychroma' }, 41 | { property: 'og:type', content: 'website' }, 42 | { property: 'og:url', content: 'https://polychroma.app/' }, 43 | { 44 | property: 'og:image', 45 | content: 'https://polychroma.app/icon.png', 46 | }, 47 | { 48 | property: 'og:description', 49 | content: 50 | 'Generate better CSS gradients using alternative colour spaces & Bezier interpolation.', 51 | }, 52 | { property: 'og:site_name', content: 'Polychroma' }, 53 | ], 54 | link: [{ rel: 'icon', type: 'image/x-icon', href: 'favicon.ico' }], 55 | }, 56 | 57 | /** Global CSS. */ 58 | css: ['~assets/css/main.css'], 59 | 60 | env: { 61 | VERSION: getVersion(), 62 | }, 63 | 64 | build: { 65 | postcss: { 66 | plugins: { 67 | autoprefixer: {}, 68 | 69 | // https://github.com/jonathantneal/postcss-advanced-variables#features 70 | 'postcss-advanced-variables': {}, 71 | 72 | // https://preset-env.cssdb.org/features 73 | 'postcss-preset-env': { 74 | stage: 0, 75 | }, 76 | }, 77 | }, 78 | }, 79 | 80 | plugins: [], 81 | 82 | modules: [ 83 | ['@nuxtjs/google-analytics', { ua: 'UA-58836125-4' }], 84 | ['@nuxtjs/markdownit', { preset: 'commonmark', typographer: true }], 85 | [ 86 | '@nuxtjs/pwa', 87 | { 88 | meta: { appleStatusBarStyle: 'black-translucent' }, 89 | manifest: { name: 'Polychroma' }, 90 | }, 91 | ], 92 | ], 93 | 94 | buildModules: ['@nuxtjs/tailwindcss'], 95 | 96 | tailwindcss: { 97 | cssPath: '~/assets/css/main.css', 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polychroma", 3 | "version": "2.1.2", 4 | "private": true, 5 | "description": "Generate better CSS gradients using alternative colour spaces & Bezier interpolation.", 6 | "author": "Jeff Nelson (https://tidaltheory.io)", 7 | "scripts": { 8 | "build": "nuxt build", 9 | "changeset": "changeset add", 10 | "deploy": "np && release && npm run generate && now dist/ -p --name polychroma && now alias", 11 | "dev": "nuxt", 12 | "generate": "nuxt generate", 13 | "lint": "npm run lint:scripts && npm run lint:styles", 14 | "lint:scripts": "eslint '**/*.{js,vue}' --fix --ignore-pattern '.nuxt/**' --ignore-pattern 'dist/**' --ignore-pattern 'static/**'", 15 | "lint:styles": "stylelint '**/*.{css,vue}' --ip '{.nuxt,dist}/**'", 16 | "start": "nuxt start", 17 | "test": "npm run lint" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "lint-staged" 22 | } 23 | }, 24 | "lint-staged": { 25 | "*.{js,vue}": "eslint --fix", 26 | "*.{css,vue}": "stylelint --fix", 27 | "package.json": "prettier --write" 28 | }, 29 | "prettier": { 30 | "htmlWhitespaceSensitivity": "strict", 31 | "semi": false, 32 | "singleQuote": true, 33 | "trailingComma": "all" 34 | }, 35 | "dependencies": { 36 | "@nuxtjs/google-analytics": "2.4.0", 37 | "@nuxtjs/markdownit": "1.2.10", 38 | "@nuxtjs/pwa": "3.3.5", 39 | "chroma-js": "2.1.0", 40 | "nuxt": "2.15.4" 41 | }, 42 | "devDependencies": { 43 | "@changesets/changelog-github": "0.3.0", 44 | "@changesets/cli": "2.14.1", 45 | "@nuxtjs/tailwindcss": "4.0.2", 46 | "@tailwindcss/typography": "0.4.0", 47 | "@zazen/eslint-config": "2.1.x", 48 | "@zazen/stylelint-config": "1.0.x", 49 | "autoprefixer": "10.2.5", 50 | "eslint": "7.14.x", 51 | "eslint-plugin-vue": "7.1.x", 52 | "husky": "4.3.x", 53 | "import-sort-style-python": "1.0.2", 54 | "lint-staged": "10.5.x", 55 | "postcss": "8.2.9", 56 | "postcss-advanced-variables": "3.0.x", 57 | "prettier": "2.1.x", 58 | "prettier-plugin-import-sort": "0.0.6", 59 | "prettier-plugin-packagejson": "2.2.x", 60 | "stylelint": "13.8.x", 61 | "tailwindcss": "2.0.4", 62 | "tailwindcss-capsize": "1.2.2" 63 | }, 64 | "engines": { 65 | "node": ">12.x" 66 | }, 67 | "importSort": { 68 | ".js,.ts,.vue": { 69 | "parser": "babylon", 70 | "style": "python", 71 | "options": { 72 | "knownFramework": [ 73 | "@nuxt", 74 | "nuxt", 75 | "@vue", 76 | "vue" 77 | ], 78 | "knownFirstparty": [ 79 | "~" 80 | ] 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pages/_.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 30 | -------------------------------------------------------------------------------- /pages/_content/about.md: -------------------------------------------------------------------------------- 1 | When defining a standard two-colour gradient, browsers 2 | interpolate using `RGB`, which can go through kind of greyish 3 | colours. `Lab` interpolation produces better, more 4 | even results. 5 | 6 | Other interpolation modes (`HSL` and `Lch` for now, working on 7 | adding more) are included as options for experimentation — the 8 | results can be overly-saturated. 9 | -------------------------------------------------------------------------------- /pages/_content/background.md: -------------------------------------------------------------------------------- 1 | The idea for Polychroma started after discovering Bugsnag’s 2 | [chromatic-sass](https://blog.bugsnag.com/chromatic-sass/) 3 | project. I loved the idea of a more natural-looking gradient, 4 | but I wanted a way to visualise the results without needing to 5 | update my dev stack. 6 | 7 | I looked into the library that powered it — the amazing 8 | [chroma.js](https://vis4.net/chromajs/) — and after tumbling 9 | down a rabbit hole of colour science and data visualisation, 10 | ended up with this simple little tool. Use it to grab a quick 11 | CSS snippet, or plug the resulting colour values into Figma, 12 | Sketch, or your image editor of choice to use in mockups. 13 | -------------------------------------------------------------------------------- /pages/_content/colophon.md: -------------------------------------------------------------------------------- 1 | Polychroma is built with [Nuxt.js](https://nuxtjs.org/) — a 2 | [Vue.js](https://vuejs.org/) framework — and 3 | [chroma.js](https://vis4.net/chromajs/) by Gregor Aisch. 4 | 5 | It uses [Tailwind CSS](https://tailwindcss.com/) for most of the styling, 6 | with a few tweaks and custom styles. 7 | 8 | The text is set in [Inter](https://rsms.me/inter/), 9 | specifically the 400, 600, and 700 weights. 10 | 11 | Comments & suggestions are very welcome! You can leave a message 12 | on [Twitter](https://twitter.com/stormwarning) or file an issue 13 | on [GitHub](https://github.com/stormwarning/polychroma). 14 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 163 | -------------------------------------------------------------------------------- /pages/info.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | 44 | 66 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/polychroma/645291f17be23d5e76a69f4607231c95bb11134a/static/favicon.ico -------------------------------------------------------------------------------- /static/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/polychroma/645291f17be23d5e76a69f4607231c95bb11134a/static/icon-1024.png -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormwarning/polychroma/645291f17be23d5e76a69f4607231c95bb11134a/static/icon.png -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | version: process.env.VERSION, 3 | colorMode: 'lab', 4 | direction: 30, 5 | colorStops: [ 6 | { 7 | color: { 8 | hex: '#000080', 9 | }, 10 | }, 11 | { 12 | color: { 13 | hex: '#ffff00', 14 | }, 15 | }, 16 | ], 17 | }) 18 | 19 | export const mutations = { 20 | CHANGE_ANGLE(state, direction) { 21 | state.direction = direction 22 | }, 23 | 24 | CHANGE_MODE(state, mode) { 25 | state.colorMode = mode 26 | }, 27 | 28 | CHANGE_STOP(state, { color, stop }) { 29 | state.colorStops[stop].color.hex = color 30 | }, 31 | } 32 | 33 | export const actions = { 34 | changeMode({ commit }, mode) { 35 | return new Promise((resolve) => { 36 | commit('CHANGE_MODE', mode) 37 | resolve() 38 | }) 39 | }, 40 | 41 | rotate({ commit }, direction) { 42 | commit('CHANGE_ANGLE', Number.parseInt(direction)) 43 | }, 44 | 45 | changeColor({ commit }, { color, stop }) { 46 | commit('CHANGE_STOP', { color, stop }) 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme') 2 | 3 | module.exports = { 4 | future: { 5 | // https://tailwindcss.com/docs/upcoming-changes#remove-deprecated-gap-utilities 6 | removeDeprecatedGapUtilities: true, 7 | // https://tailwindcss.com/docs/upcoming-changes#purge-layers-by-default 8 | purgeLayersByDefault: true, 9 | }, 10 | 11 | purge: { 12 | // Learn more on https://tailwindcss.com/docs/controlling-file-size/#removing-unused-css 13 | enabled: process.env.NODE_ENV === 'production', 14 | content: [ 15 | 'components/**/*.vue', 16 | 'layouts/**/*.vue', 17 | 'pages/**/*.vue', 18 | 'plugins/**/*.js', 19 | 'nuxt.config.js', 20 | ], 21 | }, 22 | 23 | theme: { 24 | fontMetrics: { 25 | sans: { 26 | capHeight: 2048, 27 | ascent: 2728, 28 | descent: -680, 29 | lineGap: 0, 30 | unitsPerEm: 2816, 31 | }, 32 | }, 33 | 34 | fontSize: { 35 | xs: '.75rem', 36 | sm: '.875rem', 37 | base: '1rem', 38 | lg: '1.125rem', 39 | xl: '1.25rem', 40 | '2xl': '1.5rem', 41 | '3xl': '1.875rem', 42 | '4xl': '2.25rem', 43 | '5xl': '3rem', 44 | '6xl': '4rem', 45 | }, 46 | 47 | extend: { 48 | boxShadow: { 49 | a: 50 | '0 2px 10px rgba(0, 0, 0, 0.12), 0 20px 50px 0 rgba(0, 0, 0, 0.14)', 51 | b: 52 | '0 2px 10px rgba(0, 0, 0, 0.12), 0 50px 50px 0 rgba(0, 0, 0, 0.1)', 53 | }, 54 | 55 | colors: { 56 | grey: { 57 | 100: '#f1f2f2', 58 | 200: '#e7ebed', 59 | 300: '#dee4e8', 60 | 400: '#c0c8cd', 61 | 500: '#949ca1', 62 | 600: '#62696d', 63 | 700: '#393e41', 64 | 800: '#202326', 65 | 900: '#16191b', 66 | }, 67 | }, 68 | 69 | fontFamily: { 70 | sans: ['Inter', ...defaultTheme.fontFamily.sans], 71 | }, 72 | 73 | inset: { 74 | 8: '2rem', 75 | }, 76 | 77 | letterSpacing: { 78 | mega: '0.25em', 79 | }, 80 | 81 | typography: { 82 | DEFAULT: { 83 | css: { 84 | code: { 85 | padding: '0.1875rem 0.25rem', 86 | margin: '0 1px', 87 | backgroundColor: 'rgba(0, 0, 0, 0.05)', 88 | borderRadius: '0.125rem', 89 | }, 90 | 'code::before': { content: 'none' }, 91 | 'code::after': { content: 'none' }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | 98 | plugins: [ 99 | require('@tailwindcss/typography'), 100 | require('tailwindcss-capsize').default({}), 101 | ], 102 | } 103 | -------------------------------------------------------------------------------- /utils/clipboard.js: -------------------------------------------------------------------------------- 1 | // From: https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript 2 | export function copyTextToClipboard(text) { 3 | const textArea = document.createElement('textarea') 4 | 5 | // 6 | // *** This styling is an extra step which is likely not required. *** 7 | // 8 | // Why is it here? To ensure: 9 | // 1. the element is able to have focus and selection. 10 | // 2. if element was to flash render it has minimal visual impact. 11 | // 3. less flakyness with selection and copying which **might** occur if 12 | // the textarea element is not visible. 13 | // 14 | // The likelihood is the element won't even render, not even a flash, 15 | // so some of these are just precautions. However in IE the element 16 | // is visible whilst the popup box asking the user for permission for 17 | // the web page to copy to the clipboard. 18 | // 19 | 20 | // Place in top-left corner of screen regardless of scroll position. 21 | textArea.style.position = 'fixed' 22 | textArea.style.top = 0 23 | textArea.style.left = 0 24 | 25 | // Ensure it has a small width and height. Setting to 1px / 1em 26 | // doesn't work as this gives a negative w/h on some browsers. 27 | textArea.style.width = '2em' 28 | textArea.style.height = '2em' 29 | 30 | // We don't need padding, reducing the size if it does flash render. 31 | textArea.style.padding = 0 32 | 33 | // Clean up any borders. 34 | textArea.style.border = 'none' 35 | textArea.style.outline = 'none' 36 | textArea.style.boxShadow = 'none' 37 | 38 | // Avoid flash of white box if rendered for any reason. 39 | textArea.style.background = 'transparent' 40 | 41 | textArea.value = text 42 | 43 | document.body.appendChild(textArea) 44 | 45 | textArea.select() 46 | 47 | try { 48 | document.execCommand('copy') 49 | } catch (err) { 50 | console.error('Unable to copy text') 51 | } 52 | 53 | document.body.removeChild(textArea) 54 | } 55 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "routes": [ 4 | { "handle": "filesystem" }, 5 | { "src": "/(.*)", "dest": "/$1", "continue": true } 6 | ] 7 | } 8 | --------------------------------------------------------------------------------