├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.yaml ├── .gitignore ├── .npmrc ├── .stylelintrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── convert-themes-to-rgb.js └── validate-themes.js ├── docs ├── acceptance.md ├── app-flow.md └── theme-schema.json ├── gen-environment.sh ├── package-lock.json ├── package.json ├── src ├── extension │ ├── background.js │ └── contentScript.js ├── images │ ├── arrowhead-left.svg │ ├── back-16.svg │ ├── bg_texture_01.svg │ ├── bg_texture_02.svg │ ├── bookmark-16.svg │ ├── close-16.svg │ ├── color-fb.jpg │ ├── color-twitter.jpg │ ├── forward-16.svg │ ├── home-16.svg │ ├── icon.svg │ ├── info-16.svg │ ├── logo-white.svg │ ├── logo.svg │ ├── menu-16.svg │ ├── more-16.svg │ ├── onboarding.png │ ├── patterns │ │ ├── bg-000.svg │ │ ├── bg-001.svg │ │ ├── bg-002.svg │ │ ├── bg-003.svg │ │ ├── bg-004.svg │ │ ├── bg-005.svg │ │ ├── bg-006.svg │ │ ├── bg-007.svg │ │ ├── bg-008.svg │ │ ├── bg-009.svg │ │ ├── bg-010.svg │ │ ├── bg-011.svg │ │ ├── bg-012.svg │ │ ├── bg-013.svg │ │ ├── bg-014.svg │ │ ├── bg-015.svg │ │ ├── bg-016.svg │ │ ├── bg-017.svg │ │ ├── bg-018.svg │ │ ├── bg-019.svg │ │ ├── bg-020.svg │ │ ├── bg-021.svg │ │ ├── bg-022.svg │ │ ├── bg-023.svg │ │ ├── bg-024.svg │ │ ├── bg-025.svg │ │ ├── bg-026.svg │ │ ├── bg-027.svg │ │ ├── bg-028.svg │ │ ├── bg-029.svg │ │ ├── bg-030.svg │ │ ├── bg-031.svg │ │ ├── bg-032.svg │ │ ├── bg-033.svg │ │ ├── bg-034.svg │ │ ├── bg-035.svg │ │ ├── bg-036.svg │ │ ├── bg-037.svg │ │ ├── bg-038.svg │ │ ├── bg-039.svg │ │ ├── bg-040.svg │ │ ├── bg-041.svg │ │ ├── bg-042.svg │ │ ├── bg-043.svg │ │ ├── bg-044.svg │ │ └── bg-045.svg │ ├── refresh-16.svg │ ├── sidebar-16.svg │ └── tp-facebook.jpg ├── lib │ ├── assets.js │ ├── constants.js │ ├── generators.js │ ├── store-test.js │ ├── store.js │ ├── test-setup.js │ ├── themes-test.js │ ├── themes.js │ └── utils.js ├── preset-themes │ ├── 001.json │ ├── 002.json │ ├── 003.json │ ├── 004.json │ ├── 005.json │ ├── 006.json │ ├── 007.json │ ├── 008.json │ ├── 009.json │ ├── 010.json │ ├── 011.json │ ├── 012.json │ ├── 013.json │ ├── 014.json │ ├── 015.json │ ├── 016.json │ ├── 017.json │ ├── 018.json │ ├── 019.json │ ├── 020.json │ ├── 021.json │ ├── 022.json │ ├── default.json │ └── hotdog.json └── web │ ├── favicon.ico │ ├── index.html.ejs │ ├── index.js │ ├── index.scss │ ├── lib │ ├── _common.scss │ ├── _picker.scss │ ├── colors.scss │ ├── components │ │ ├── App │ │ │ ├── flat-firefox.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── AppBackground │ │ │ ├── AppBackground-test.js │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── AppFooter │ │ │ ├── AppFooter-test.js │ │ │ ├── github-logo.svg │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ ├── moz-logo.svg │ │ │ └── twitter-logo.svg │ │ ├── AppHeader │ │ │ ├── AppHeader-test.js │ │ │ ├── icon_export.svg │ │ │ ├── icon_forget.svg │ │ │ ├── icon_heart.svg │ │ │ ├── icon_randomize.svg │ │ │ ├── icon_redo.svg │ │ │ ├── icon_share.svg │ │ │ ├── icon_undo.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── AppLoadingIndicator │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── Banner │ │ │ ├── firefox.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── Browser │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── BrowserChrome │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── BrowserPopup │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── BrowserTabs │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── BrowserTools │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ClearImageModal │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ExportThemeDialog │ │ │ └── index.js │ │ ├── GeneratorButtons │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── Mobile │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── Modal │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── Onboarding │ │ │ ├── LogoIcon.js │ │ │ ├── SparklesIcon.js │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── PaginatedThemeSelector │ │ │ ├── arrow_left.svg │ │ │ ├── arrow_right.svg │ │ │ ├── close.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── PresetThemeSelector │ │ │ └── index.js │ │ ├── SavedThemeSelector │ │ │ └── index.js │ │ ├── SharedThemeDialog │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ └── stars-bg.svg │ │ ├── StorageSpaceInformation │ │ │ ├── StorageIcon.js │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── TermsPrivacyModal │ │ │ └── index.js │ │ ├── ThemeBackgroundPicker │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemeBuilder │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemeColorsEditor │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemeCustomBackgroundPicker │ │ │ ├── arrow-down.svg │ │ │ ├── close.svg │ │ │ ├── icon_align_center.svg │ │ │ ├── icon_align_left.svg │ │ │ ├── icon_drag.svg │ │ │ ├── icon_loading.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemeLogger │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemePatternPicker │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── ThemeSaveButton │ │ │ └── index.js │ │ └── ThemeUrl │ │ │ ├── index.js │ │ │ └── index.scss │ ├── export.js │ ├── middleware.js │ └── storage.js │ ├── robots.txt │ └── testing.html ├── webpack.common.js ├── webpack.extension.js └── webpack.web.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | indent_size = 2 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - plugin:import/recommended 4 | - plugin:mozilla/recommended 5 | - plugin:react/recommended 6 | 7 | env: 8 | browser: true 9 | es6: true 10 | jquery: true 11 | 12 | settings: 13 | react: 14 | version: "16.5.0" 15 | 16 | plugins: 17 | - babel 18 | - mozilla 19 | 20 | parser: babel-eslint 21 | 22 | parserOptions: 23 | ecmaVersion: 6 24 | 25 | root: true 26 | 27 | rules: 28 | strict: 0 29 | no-trailing-spaces: off 30 | eol-last: off 31 | arrow-parens: off 32 | comma-dangle: off 33 | function-paren-newline: off 34 | guard-for-in: off 35 | import/no-named-as-default: off 36 | import/prefer-default-export: off 37 | linebreak-style: off 38 | no-multi-assign: off 39 | no-plusplus: off 40 | no-restricted-syntax: off 41 | no-use-before-define: off 42 | object-curly-newline: off 43 | react/prop-types: off 44 | mozilla/no-define-cc-etc: off 45 | 46 | overrides: 47 | - 48 | files: ['src/**/*.js'] 49 | globals: 50 | process: true 51 | require: true 52 | module: true 53 | - 54 | files: ['src/extension/**/*.js'] 55 | env: 56 | webextensions: true 57 | - 58 | files: ['webpack.*.js','bin/*.js'] 59 | rules: 60 | import/unambiguous: off 61 | env: 62 | node: true 63 | - 64 | files: 65 | - 'src/lib/test-setup.js' 66 | - 'src/**/*-test.js' 67 | rules: 68 | import/unambiguous: off 69 | env: 70 | node: true 71 | mocha: true 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File types to ignore globally 2 | *.DS_Store 3 | *.sw? 4 | *~ 5 | 6 | npm-debug.log 7 | 8 | # Directories to ignore 9 | *.xpi 10 | *.zip 11 | build/ 12 | node_modules/ 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard" 4 | ], 5 | "rules": { 6 | "at-rule-no-unknown": null, 7 | "custom-property-empty-line-before": null, 8 | "no-descending-specificity": null, 9 | "no-empty-source": null, 10 | "number-leading-zero": "never" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /bin/convert-themes-to-rgb.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const tinycolor = require("tinycolor2"); 4 | const colorsWithoutAlpha = ["tab_background_text", "frame", "sidebar"]; 5 | const themesPath = path.join(__dirname, "..", "src", "preset-themes"); 6 | 7 | const makeTinycolor = colorIn => { 8 | let { a, s } = colorIn; 9 | let newColor = tinycolor({ 10 | ...colorIn, 11 | s: Math.floor(s) / 100.0 12 | }); 13 | if (a != undefined) { 14 | newColor.a = Math.floor(a) / 100.0; 15 | } 16 | return newColor; 17 | }; 18 | 19 | fs.readdirSync(themesPath) 20 | .filter(filename => path.extname(filename) === ".json") 21 | .forEach(filename => { 22 | const data = fs.readFileSync(path.join(themesPath, filename), "utf8"); 23 | const theme = JSON.parse(data); 24 | Object.entries(theme.colors).forEach(([name, color]) => { 25 | const rgba = makeTinycolor(color).toRgb(); 26 | if ( 27 | colorsWithoutAlpha.includes(name) || 28 | !("a" in color) || 29 | rgba.a === 1 30 | ) { 31 | delete rgba.a; 32 | } 33 | theme.colors[name] = rgba; 34 | }); 35 | fs.writeFileSync( 36 | path.join(themesPath, filename), 37 | JSON.stringify(theme, null, " "), 38 | "utf8" 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /bin/validate-themes.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const Ajv = require("ajv"); 4 | 5 | const themesPath = path.join(__dirname, "..", "src", "preset-themes"); 6 | const schemaFilename = path.join(__dirname, "..", "docs", "theme-schema.json"); 7 | const schema = JSON.parse(fs.readFileSync(schemaFilename, "utf8")); 8 | 9 | const ajv = new Ajv({ allErrors: true }); 10 | const validate = ajv.compile(schema); 11 | 12 | let foundInvalid = false; 13 | 14 | fs.readdirSync(themesPath) 15 | .filter(filename => path.extname(filename) === ".json") 16 | .forEach(filename => { 17 | const data = fs.readFileSync(path.join(themesPath, filename), "utf8"); 18 | const theme = JSON.parse(data); 19 | if (!validate(theme)) { 20 | console.log( 21 | "Theme validation failed for", 22 | filename, 23 | ajv.errorsText(validate.errors) 24 | ); // eslint-disable-line no-console 25 | foundInvalid = true; 26 | } 27 | }); 28 | 29 | if (foundInvalid) { 30 | console.log("Found invalid theme JSON data"); // eslint-disable-line no-console 31 | process.exit(1); 32 | } 33 | -------------------------------------------------------------------------------- /docs/app-flow.md: -------------------------------------------------------------------------------- 1 | # App data flow 2 | 3 | Theme param is extracted from the url search query. 4 | 5 | If no theme param, fire off a message to request current theme from add-on. The add-on may never answer, so start the loader delay. Set timeout for `LOADER_DELAY_PERIOD` and once the timer expires, dispatch action `actions.ui.setLoaderDelayExpired(true)` . 6 | 7 | If theme param present, 8 | 9 | - decode param to get the `theme`. 10 | 11 | - If the theme is decoded, shared theme is received. 12 | 13 | * Dispatch action `{...actions.theme.setTheme({theme})}` to set the theme to current editor theme. But for this theme, skip updating history(because it came from the url) and add-on updates(because it needs approval). 14 | 15 | * Dispatch action `actions.ui.setPendingTheme({ theme })` to store the theme as pending. 16 | 17 | * Fire off a message to request current theme from the add-on. If the add-on never responds with a current theme, the shared theme just appears in the editor. If the add-on is installed and responds with a current theme, then that current theme is loaded into the editor. 18 | 19 | * If the pending theme exists and is not identical to the current theme and `state.ui.userHasEdited` is false, the "pending" shared theme is presented in an approval dialog with a preview (i.e. SharedThemeDialog). 20 | * From there, 21 | - the user can apply the shared theme by dispatching action `{...actions.theme.setTheme({theme: pendingTheme }), meta: { userEdit: true }}` to override the current theme. 22 | or 23 | - skip it and discard by dispatching action `actions.ui.clearPendingTheme()`. 24 | 25 | - If theme decoding fails, ignore it. 26 | -------------------------------------------------------------------------------- /gen-environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${CIRCLE_BRANCH}" == "development" ] 4 | then 5 | SITE_URL="https://mozilla.github.io/FirefoxColor/" 6 | ADDON_URL="testing.html" 7 | elif [ "${CIRCLE_BRANCH}" == "stage" ] 8 | then 9 | SITE_URL="https://color.stage.mozaws.net/" 10 | ADDON_URL="testing.html" 11 | elif [ "${CIRCLE_BRANCH}" == "production" ] 12 | then 13 | SITE_URL="https://color.firefox.com/" 14 | ADDON_URL="https://addons.mozilla.org/firefox/addon/firefox-color/" 15 | else 16 | echo "Unknown branch: ${CIRCLE_BRANCH}" 17 | fi 18 | 19 | # This will be appended to $BASH_ENV in .circleci/config.yml 20 | cat < { 9 | port = null; 10 | reconnect(); 11 | }); 12 | port.onMessage.addListener(message => { 13 | window.postMessage( 14 | { 15 | ...message, 16 | channel: `${CHANNEL_NAME}-web` 17 | }, 18 | "*" 19 | ); 20 | }); 21 | } 22 | 23 | // HACK: try reconnecting when reloaded from about:debugging 24 | function reconnect() { 25 | setTimeout(() => { 26 | if (port) { 27 | return; 28 | } 29 | connect(); 30 | reconnect(); 31 | }, 1000); 32 | } 33 | 34 | // Relay content messages to backend port if the channel name matches 35 | // (Not a security feature so much as a noise filter) 36 | window.addEventListener("message", event => { 37 | if ( 38 | port && 39 | event.source === window && 40 | event.data && 41 | event.data.channel === `${CHANNEL_NAME}-extension` 42 | ) { 43 | port.postMessage({ 44 | ...event.data, 45 | location: window.location.href 46 | }); 47 | } 48 | }); 49 | 50 | connect(); 51 | -------------------------------------------------------------------------------- /src/images/arrowhead-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/back-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/bg_texture_01.svg: -------------------------------------------------------------------------------- 1 | bg_texture -------------------------------------------------------------------------------- /src/images/bookmark-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/close-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/color-fb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/FirefoxColor/2738144bb995d5c3dd0fd604f0e9c9a746103363/src/images/color-fb.jpg -------------------------------------------------------------------------------- /src/images/color-twitter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/FirefoxColor/2738144bb995d5c3dd0fd604f0e9c9a746103363/src/images/color-twitter.jpg -------------------------------------------------------------------------------- /src/images/forward-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/home-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/icon.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /src/images/info-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 2 | -------------------------------------------------------------------------------- /src/images/menu-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | menu-16 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/images/more-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/FirefoxColor/2738144bb995d5c3dd0fd604f0e9c9a746103363/src/images/onboarding.png -------------------------------------------------------------------------------- /src/images/patterns/bg-000.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-001.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-002.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-003.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-004.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-005.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-006.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-007.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-008.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-009.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-010.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-011.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-012.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-013.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-014.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-015.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-016.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-017.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-024.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-025.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-026.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-027.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-028.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-029.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-030.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-031.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-032.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-033.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-036.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-037.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-038.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-039.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-040.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-041.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-042.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/patterns/bg-044.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/refresh-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/sidebar-16.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/tp-facebook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/FirefoxColor/2738144bb995d5c3dd0fd604f0e9c9a746103363/src/images/tp-facebook.jpg -------------------------------------------------------------------------------- /src/lib/assets.js: -------------------------------------------------------------------------------- 1 | export const presetThemesContext = require.context( 2 | "../preset-themes/", 3 | false, 4 | /.*\.json/ 5 | ); 6 | 7 | export const bgImages = require.context( 8 | "../images/patterns/", 9 | false, 10 | /bg-.*\.svg/ 11 | ); 12 | 13 | export const buttonImages = require.context("../images/", false, /.*-16\.svg/); 14 | -------------------------------------------------------------------------------- /src/lib/generators.js: -------------------------------------------------------------------------------- 1 | import tinycolor from "tinycolor2"; 2 | import { normalizeTheme } from "./themes"; 3 | import { presetThemesContext } from "./assets"; 4 | const defaultTheme = presetThemesContext("./default.json"); 5 | 6 | const WCAG_AA = 4.5; 7 | const MAX_CONTRAST_RATIO = 21; 8 | 9 | const cloneDefault = () => { 10 | return JSON.parse(JSON.stringify(defaultTheme)); 11 | }; 12 | 13 | export const generateRandomTheme = () => { 14 | const newTheme = cloneDefault(); 15 | 16 | Object.keys(newTheme.colors).map(key => { 17 | newTheme.colors[key] = tinycolor.random().toRgb(); 18 | }); 19 | 20 | return normalizeTheme(newTheme); 21 | }; 22 | 23 | export const generateComplementaryTheme = (color = null) => { 24 | const newTheme = cloneDefault(); 25 | const seed = color === null ? tinycolor.random() : tinycolor(color); 26 | const baseColor = seed.toRgb(); 27 | const lightColor = seed.lighten(5).toRgb(); 28 | const complementColor = createA11yColor(seed.complement().toRgb(), baseColor); 29 | 30 | newTheme.colors.toolbar = baseColor; 31 | newTheme.colors.toolbar_text = complementColor; 32 | newTheme.colors.frame = lightColor; 33 | newTheme.colors.tab_background_text = complementColor; 34 | newTheme.colors.toolbar_field = lightColor; 35 | newTheme.colors.toolbar_field_text = complementColor; 36 | newTheme.colors.tab_line = complementColor; 37 | newTheme.colors.popup = lightColor; 38 | newTheme.colors.popup_text = complementColor; 39 | 40 | return normalizeTheme(newTheme); 41 | }; 42 | 43 | const createA11yColor = (testColor, comparisonColor) => { 44 | // Just return if A11y already 45 | if (tinycolor.isReadable(testColor, comparisonColor)) { 46 | return testColor; 47 | } 48 | 49 | let a11yColor = null; 50 | let minValidRatio = MAX_CONTRAST_RATIO; 51 | 52 | // Otherwise, create an array of colors with the same hue as the test color 53 | // and get the one closest to, but above the WCAG_AA Limit 54 | tinycolor(testColor) 55 | .monochromatic() 56 | .filter(function(color) { 57 | const ratio = tinycolor.readability(color, comparisonColor); 58 | if (ratio < minValidRatio && ratio >= WCAG_AA) { 59 | a11yColor = color; 60 | minValidRatio = ratio; 61 | } 62 | }); 63 | 64 | // If all else fails, check for a dark color and make the a11y color white or black 65 | if (a11yColor === null) { 66 | a11yColor = tinycolor(comparisonColor).isDark() 67 | ? tinycolor("white") 68 | : tinycolor("black"); 69 | } 70 | 71 | return a11yColor.toRgb(); 72 | }; 73 | -------------------------------------------------------------------------------- /src/lib/test-setup.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | require("@babel/register")({ 5 | presets: [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | targets: ["last 2 versions"] 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | plugins: [ 15 | "@babel/plugin-proposal-object-rest-spread", 16 | "@babel/plugin-proposal-class-properties" 17 | ] 18 | }); 19 | 20 | require.extensions[".svg"] = () => null; 21 | require.extensions[".scss"] = () => null; 22 | 23 | const mockRequire = require("mock-require"); 24 | 25 | // Quick & dirty simulation of Webpack's require.context() 26 | const mockContext = (itemsPath, filterFn, loadFn) => { 27 | const items = fs 28 | .readdirSync(itemsPath) 29 | .filter(filterFn) 30 | .reduce( 31 | (out, filename) => ({ 32 | ...out, 33 | [`./${filename}`]: loadFn(filename) 34 | }), 35 | {} 36 | ); 37 | const fn = key => items[key]; 38 | fn.keys = () => Object.keys(items); 39 | return fn; 40 | }; 41 | 42 | const presetThemesPath = path.join(__dirname, "..", "preset-themes"); 43 | 44 | mockRequire("./assets", { 45 | presetThemesContext: mockContext( 46 | presetThemesPath, 47 | filename => path.extname(filename) === ".json", 48 | filename => 49 | JSON.parse(fs.readFileSync(path.join(presetThemesPath, filename), "utf8")) 50 | ), 51 | bgImages: mockContext( 52 | path.join(__dirname, "..", "images", "patterns"), 53 | filename => path.extname(filename) === ".svg", 54 | () => "FAKE IMAGE DATA" 55 | ), 56 | buttomImages: mockContext( 57 | path.join(__dirname, "..", "images"), 58 | filename => filename.includes("-16.svg"), 59 | () => "FAKE IMAGE DATA" 60 | ) 61 | }); 62 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | export const DEBUG = process.env.NODE_ENV === "development"; 2 | 3 | export const makeLog = context => (...args) => 4 | // eslint-disable-next-line no-console 5 | DEBUG && console.log(`[FirefoxColor ${context}]`, ...args); 6 | 7 | export const getCustomImages = (backgroundImages = [], images = []) => { 8 | return backgroundImages 9 | .map(item => { 10 | let customImage = { ...item }; 11 | let image = JSON.parse(localStorage.getItem(`IMAGE-${item.name}`)); 12 | if (image) { 13 | customImage.image = images[item.name] && images[item.name].image; 14 | return customImage; 15 | } 16 | return null; 17 | }) 18 | .filter(Boolean); 19 | }; 20 | -------------------------------------------------------------------------------- /src/preset-themes/001.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": { 3 | "toolbar": { 4 | "r": 43, 5 | "g": 206, 6 | "b": 227 7 | }, 8 | "toolbar_text": { 9 | "r": 215, 10 | "g": 226, 11 | "b": 239 12 | }, 13 | "frame": { 14 | "r": 115, 15 | "g": 214, 16 | "b": 228 17 | }, 18 | "tab_background_text": { 19 | "r": 84, 20 | "g": 84, 21 | "b": 84 22 | }, 23 | "toolbar_field": { 24 | "r": 255, 25 | "g": 255, 26 | "b": 255, 27 | "a": 0.53 28 | }, 29 | "toolbar_field_text": { 30 | "r": 255, 31 | "g": 71, 32 | "b": 203 33 | }, 34 | "tab_line": { 35 | "r": 255, 36 | "g": 66, 37 | "b": 205 38 | }, 39 | "popup": { 40 | "r": 231, 41 | "g": 242, 42 | "b": 244 43 | }, 44 | "popup_text": { 45 | "r": 84, 46 | "g": 84, 47 | "b": 84 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": ["./bg-024.svg"] 52 | }, 53 | "title": "001" 54 | } 55 | -------------------------------------------------------------------------------- /src/preset-themes/002.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "002", 3 | "colors": { 4 | "toolbar": { 5 | "r": 7, 6 | "g": 50, 7 | "b": 84 8 | }, 9 | "toolbar_text": { 10 | "r": 67, 11 | "g": 194, 12 | "b": 218 13 | }, 14 | "frame": { 15 | "r": 14, 16 | "g": 41, 17 | "b": 65 18 | }, 19 | "tab_background_text": { 20 | "r": 24, 21 | "g": 156, 22 | "b": 180 23 | }, 24 | "toolbar_field": { 25 | "r": 38, 26 | "g": 72, 27 | "b": 102 28 | }, 29 | "toolbar_field_text": { 30 | "r": 67, 31 | "g": 194, 32 | "b": 218 33 | }, 34 | "tab_line": { 35 | "r": 67, 36 | "g": 194, 37 | "b": 218 38 | }, 39 | "popup": { 40 | "r": 7, 41 | "g": 50, 42 | "b": 84 43 | }, 44 | "popup_text": { 45 | "r": 24, 46 | "g": 156, 47 | "b": 180 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/preset-themes/003.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": { 3 | "toolbar": { 4 | "r": 58, 5 | "g": 89, 6 | "b": 105, 7 | "a": 0.17 8 | }, 9 | "toolbar_text": { 10 | "r": 247, 11 | "g": 253, 12 | "b": 251 13 | }, 14 | "frame": { 15 | "r": 157, 16 | "g": 183, 17 | "b": 190 18 | }, 19 | "tab_background_text": { 20 | "r": 255, 21 | "g": 255, 22 | "b": 255 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 90, 31 | "g": 93, 32 | "b": 154 33 | }, 34 | "tab_line": { 35 | "r": 218, 36 | "g": 135, 37 | "b": 124 38 | }, 39 | "popup": { 40 | "r": 255, 41 | "g": 255, 42 | "b": 255 43 | }, 44 | "popup_text": { 45 | "r": 90, 46 | "g": 93, 47 | "b": 154 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-036.svg" 53 | ] 54 | }, 55 | "title": "003" 56 | } 57 | -------------------------------------------------------------------------------- /src/preset-themes/004.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "004", 3 | "colors": { 4 | "toolbar": { 5 | "r": 31, 6 | "g": 34, 7 | "b": 61 8 | }, 9 | "toolbar_text": { 10 | "r": 255, 11 | "g": 255, 12 | "b": 255 13 | }, 14 | "frame": { 15 | "r": 8, 16 | "g": 11, 17 | "b": 33 18 | }, 19 | "tab_background_text": { 20 | "r": 215, 21 | "g": 226, 22 | "b": 239 23 | }, 24 | "toolbar_field": { 25 | "r": 54, 26 | "g": 58, 27 | "b": 89 28 | }, 29 | "toolbar_field_text": { 30 | "r": 255, 31 | "g": 255, 32 | "b": 255 33 | }, 34 | "tab_line": { 35 | "r": 205, 36 | "g": 35, 37 | "b": 185 38 | }, 39 | "popup": { 40 | "r": 31, 41 | "g": 34, 42 | "b": 61 43 | }, 44 | "popup_text": { 45 | "r": 215, 46 | "g": 226, 47 | "b": 239 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/preset-themes/005.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "005", 3 | "colors": { 4 | "toolbar": { 5 | "r": 236, 6 | "g": 220, 7 | "b": 249 8 | }, 9 | "toolbar_text": { 10 | "r": 159, 11 | "g": 65, 12 | "b": 190 13 | }, 14 | "frame": { 15 | "r": 223, 16 | "g": 182, 17 | "b": 246 18 | }, 19 | "tab_background_text": { 20 | "r": 159, 21 | "g": 65, 22 | "b": 190 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255, 28 | "a": 0 29 | }, 30 | "toolbar_field_text": { 31 | "r": 159, 32 | "g": 65, 33 | "b": 190 34 | }, 35 | "tab_line": { 36 | "r": 253, 37 | "g": 124, 38 | "b": 73 39 | }, 40 | "popup": { 41 | "r": 236, 42 | "g": 220, 43 | "b": 249 44 | }, 45 | "popup_text": { 46 | "r": 159, 47 | "g": 65, 48 | "b": 190 49 | } 50 | }, 51 | "images": { 52 | "additional_backgrounds": [ 53 | "./bg-014.svg" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/preset-themes/006.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "006", 3 | "colors": { 4 | "toolbar": { 5 | "r": 243, 6 | "g": 244, 7 | "b": 247 8 | }, 9 | "toolbar_text": { 10 | "r": 185, 11 | "g": 17, 12 | "b": 223 13 | }, 14 | "frame": { 15 | "r": 255, 16 | "g": 255, 17 | "b": 255 18 | }, 19 | "tab_background_text": { 20 | "r": 185, 21 | "g": 17, 22 | "b": 223 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 185, 31 | "g": 17, 32 | "b": 223 33 | }, 34 | "tab_line": { 35 | "r": 185, 36 | "g": 17, 37 | "b": 223 38 | }, 39 | "popup": { 40 | "r": 243, 41 | "g": 244, 42 | "b": 247 43 | }, 44 | "popup_text": { 45 | "r": 185, 46 | "g": 17, 47 | "b": 223 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/preset-themes/007.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "007", 3 | "colors": { 4 | "toolbar": { 5 | "r": 22, 6 | "g": 72, 7 | "b": 75, 8 | "a": 0.8 9 | }, 10 | "toolbar_text": { 11 | "r": 27, 12 | "g": 170, 13 | "b": 177 14 | }, 15 | "frame": { 16 | "r": 22, 17 | "g": 54, 18 | "b": 75 19 | }, 20 | "tab_background_text": { 21 | "r": 148, 22 | "g": 157, 23 | "b": 173 24 | }, 25 | "toolbar_field": { 26 | "r": 0, 27 | "g": 0, 28 | "b": 0, 29 | "a": 0.2 30 | }, 31 | "toolbar_field_text": { 32 | "r": 251, 33 | "g": 131, 34 | "b": 165 35 | }, 36 | "tab_line": { 37 | "r": 28, 38 | "g": 171, 39 | "b": 176 40 | }, 41 | "popup": { 42 | "r": 22, 43 | "g": 54, 44 | "b": 75 45 | }, 46 | "popup_text": { 47 | "r": 251, 48 | "g": 131, 49 | "b": 165 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/008.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "008", 3 | "colors": { 4 | "toolbar": { 5 | "r": 219, 6 | "g": 220, 7 | "b": 219 8 | }, 9 | "toolbar_text": { 10 | "r": 136, 11 | "g": 141, 12 | "b": 129 13 | }, 14 | "frame": { 15 | "r": 201, 16 | "g": 197, 17 | "b": 197 18 | }, 19 | "tab_background_text": { 20 | "r": 245, 21 | "g": 78, 22 | "b": 41 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255, 28 | "a": 0.7 29 | }, 30 | "toolbar_field_text": { 31 | "r": 132, 32 | "g": 168, 33 | "b": 67 34 | }, 35 | "tab_line": { 36 | "r": 245, 37 | "g": 75, 38 | "b": 41 39 | }, 40 | "popup": { 41 | "r": 219, 42 | "g": 220, 43 | "b": 219 44 | }, 45 | "popup_text": { 46 | "r": 87, 47 | "g": 120, 48 | "b": 33 49 | } 50 | }, 51 | "images": { 52 | "additional_backgrounds": [] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/preset-themes/009.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "009", 3 | "colors": { 4 | "toolbar": { 5 | "r": 248, 6 | "g": 237, 7 | "b": 236 8 | }, 9 | "toolbar_text": { 10 | "r": 140, 11 | "g": 89, 12 | "b": 87 13 | }, 14 | "frame": { 15 | "r": 243, 16 | "g": 223, 17 | "b": 223 18 | }, 19 | "tab_background_text": { 20 | "r": 140, 21 | "g": 89, 22 | "b": 87 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 140, 31 | "g": 89, 32 | "b": 87 33 | }, 34 | "tab_line": { 35 | "r": 140, 36 | "g": 89, 37 | "b": 87 38 | }, 39 | "popup": { 40 | "r": 248, 41 | "g": 237, 42 | "b": 236 43 | }, 44 | "popup_text": { 45 | "r": 140, 46 | "g": 89, 47 | "b": 87 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/preset-themes/010.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "010", 3 | "colors": { 4 | "toolbar": { 5 | "r": 245, 6 | "g": 255, 7 | "b": 252 8 | }, 9 | "toolbar_text": { 10 | "r": 0, 11 | "g": 0, 12 | "b": 0 13 | }, 14 | "frame": { 15 | "r": 215, 16 | "g": 234, 17 | "b": 234 18 | }, 19 | "tab_background_text": { 20 | "r": 0, 21 | "g": 0, 22 | "b": 0 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 0, 31 | "g": 0, 32 | "b": 0 33 | }, 34 | "tab_line": { 35 | "r": 0, 36 | "g": 255, 37 | "b": 200 38 | }, 39 | "popup": { 40 | "r": 245, 41 | "g": 255, 42 | "b": 252 43 | }, 44 | "popup_text": { 45 | "r": 0, 46 | "g": 0, 47 | "b": 0 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-045.svg" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/011.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "011", 3 | "colors": { 4 | "toolbar": { 5 | "r": 235, 6 | "g": 235, 7 | "b": 235 8 | }, 9 | "toolbar_text": { 10 | "r": 88, 11 | "g": 151, 12 | "b": 223 13 | }, 14 | "frame": { 15 | "r": 201, 16 | "g": 212, 17 | "b": 222 18 | }, 19 | "tab_background_text": { 20 | "r": 240, 21 | "g": 140, 22 | "b": 40 23 | }, 24 | "toolbar_field": { 25 | "r": 248, 26 | "g": 241, 27 | "b": 211 28 | }, 29 | "toolbar_field_text": { 30 | "r": 118, 31 | "g": 132, 32 | "b": 98 33 | }, 34 | "tab_line": { 35 | "r": 255, 36 | "g": 51, 37 | "b": 51 38 | }, 39 | "popup": { 40 | "r": 235, 41 | "g": 235, 42 | "b": 235 43 | }, 44 | "popup_text": { 45 | "r": 255, 46 | "g": 51, 47 | "b": 51 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-020.svg" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/012.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "012", 3 | "colors": { 4 | "toolbar": { 5 | "r": 233, 6 | "g": 237, 7 | "b": 240 8 | }, 9 | "toolbar_text": { 10 | "r": 214, 11 | "g": 81, 12 | "b": 109 13 | }, 14 | "frame": { 15 | "r": 73, 16 | "g": 153, 17 | "b": 201 18 | }, 19 | "tab_background_text": { 20 | "r": 248, 21 | "g": 248, 22 | "b": 248 23 | }, 24 | "toolbar_field": { 25 | "r": 252, 26 | "g": 252, 27 | "b": 252 28 | }, 29 | "toolbar_field_text": { 30 | "r": 214, 31 | "g": 81, 32 | "b": 109 33 | }, 34 | "tab_line": { 35 | "r": 214, 36 | "g": 81, 37 | "b": 109 38 | }, 39 | "popup": { 40 | "r": 233, 41 | "g": 237, 42 | "b": 240 43 | }, 44 | "popup_text": { 45 | "r": 177, 46 | "g": 63, 47 | "b": 87 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-011.svg" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/013.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "013", 3 | "colors": { 4 | "toolbar": { 5 | "r": 225, 6 | "g": 234, 7 | "b": 239, 8 | "a": 0.41 9 | }, 10 | "toolbar_text": { 11 | "r": 255, 12 | "g": 138, 13 | "b": 163 14 | }, 15 | "frame": { 16 | "r": 60, 17 | "g": 105, 18 | "b": 134 19 | }, 20 | "tab_background_text": { 21 | "r": 255, 22 | "g": 138, 23 | "b": 163 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255 29 | }, 30 | "toolbar_field_text": { 31 | "r": 123, 32 | "g": 127, 33 | "b": 204 34 | }, 35 | "tab_line": { 36 | "r": 255, 37 | "g": 138, 38 | "b": 163 39 | }, 40 | "popup": { 41 | "r": 225, 42 | "g": 234, 43 | "b": 239 44 | }, 45 | "popup_text": { 46 | "r": 60, 47 | "g": 105, 48 | "b": 134 49 | } 50 | }, 51 | "images": { 52 | "additional_backgrounds": ["./bg-031.svg"] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/preset-themes/014.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "014", 3 | "colors": { 4 | "toolbar": { 5 | "r": 251, 6 | "g": 203, 7 | "b": 251, 8 | "a": 0.86 9 | }, 10 | "toolbar_text": { 11 | "r": 102, 12 | "g": 102, 13 | "b": 194 14 | }, 15 | "frame": { 16 | "r": 247, 17 | "g": 212, 18 | "b": 212 19 | }, 20 | "tab_background_text": { 21 | "r": 104, 22 | "g": 104, 23 | "b": 196 24 | }, 25 | "toolbar_field": { 26 | "r": 102, 27 | "g": 118, 28 | "b": 225, 29 | "a": 0.26 30 | }, 31 | "toolbar_field_text": { 32 | "r": 255, 33 | "g": 255, 34 | "b": 255 35 | }, 36 | "tab_line": { 37 | "r": 102, 38 | "g": 102, 39 | "b": 194 40 | }, 41 | "popup": { 42 | "r": 245, 43 | "g": 226, 44 | "b": 226 45 | }, 46 | "popup_text": { 47 | "r": 104, 48 | "g": 104, 49 | "b": 196 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-030.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/015.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "015", 3 | "colors": { 4 | "toolbar": { 5 | "r": 27, 6 | "g": 40, 7 | "b": 45 8 | }, 9 | "toolbar_text": { 10 | "r": 143, 11 | "g": 143, 12 | "b": 143 13 | }, 14 | "frame": { 15 | "r": 0, 16 | "g": 0, 17 | "b": 0 18 | }, 19 | "tab_background_text": { 20 | "r": 140, 21 | "g": 140, 22 | "b": 140 23 | }, 24 | "toolbar_field": { 25 | "r": 0, 26 | "g": 0, 27 | "b": 0 28 | }, 29 | "toolbar_field_text": { 30 | "r": 255, 31 | "g": 255, 32 | "b": 255 33 | }, 34 | "tab_line": { 35 | "r": 35, 36 | "g": 205, 37 | "b": 179 38 | }, 39 | "popup": { 40 | "r": 27, 41 | "g": 40, 42 | "b": 45 43 | }, 44 | "popup_text": { 45 | "r": 140, 46 | "g": 140, 47 | "b": 140 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-026.svg" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/016.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "016", 3 | "colors": { 4 | "toolbar": { 5 | "r": 220, 6 | "g": 181, 7 | "b": 253, 8 | "a": 0.86 9 | }, 10 | "toolbar_text": { 11 | "r": 255, 12 | "g": 255, 13 | "b": 255 14 | }, 15 | "frame": { 16 | "r": 220, 17 | "g": 203, 18 | "b": 216 19 | }, 20 | "tab_background_text": { 21 | "r": 0, 22 | "g": 0, 23 | "b": 0 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 0.21 30 | }, 31 | "toolbar_field_text": { 32 | "r": 255, 33 | "g": 255, 34 | "b": 255 35 | }, 36 | "tab_line": { 37 | "r": 255, 38 | "g": 255, 39 | "b": 255 40 | }, 41 | "popup": { 42 | "r": 234, 43 | "g": 235, 44 | "b": 246 45 | }, 46 | "popup_text": { 47 | "r": 153, 48 | "g": 51, 49 | "b": 255 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-043.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/017.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "017", 3 | "colors": { 4 | "toolbar": { 5 | "r": 234, 6 | "g": 235, 7 | "b": 246 8 | }, 9 | "toolbar_text": { 10 | "r": 255, 11 | "g": 138, 12 | "b": 163 13 | }, 14 | "frame": { 15 | "r": 233, 16 | "g": 225, 17 | "b": 234 18 | }, 19 | "tab_background_text": { 20 | "r": 255, 21 | "g": 138, 22 | "b": 163 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 153, 31 | "g": 51, 32 | "b": 255 33 | }, 34 | "tab_line": { 35 | "r": 153, 36 | "g": 51, 37 | "b": 255 38 | }, 39 | "popup": { 40 | "r": 234, 41 | "g": 235, 42 | "b": 246 43 | }, 44 | "popup_text": { 45 | "r": 153, 46 | "g": 51, 47 | "b": 255 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": ["./bg-012.svg"] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/preset-themes/018.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "018", 3 | "colors": { 4 | "toolbar": { 5 | "r": 181, 6 | "g": 242, 7 | "b": 253, 8 | "a": 0.77 9 | }, 10 | "toolbar_text": { 11 | "r": 255, 12 | "g": 255, 13 | "b": 255 14 | }, 15 | "frame": { 16 | "r": 225, 17 | "g": 86, 18 | "b": 193 19 | }, 20 | "tab_background_text": { 21 | "r": 0, 22 | "g": 15, 23 | "b": 1 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 0.21 30 | }, 31 | "toolbar_field_text": { 32 | "r": 221, 33 | "g": 8, 34 | "b": 175 35 | }, 36 | "tab_line": { 37 | "r": 255, 38 | "g": 255, 39 | "b": 255 40 | }, 41 | "popup": { 42 | "r": 255, 43 | "g": 255, 44 | "b": 255 45 | }, 46 | "popup_text": { 47 | "r": 0, 48 | "g": 15, 49 | "b": 1 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-041.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/019.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "019", 3 | "colors": { 4 | "toolbar": { 5 | "r": 255, 6 | "g": 255, 7 | "b": 255, 8 | "a": 0.64 9 | }, 10 | "toolbar_text": { 11 | "r": 0, 12 | "g": 187, 13 | "b": 255 14 | }, 15 | "frame": { 16 | "r": 221, 17 | "g": 249, 18 | "b": 118 19 | }, 20 | "tab_background_text": { 21 | "r": 0, 22 | "g": 187, 23 | "b": 255 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 0.36 30 | }, 31 | "toolbar_field_text": { 32 | "r": 0, 33 | "g": 183, 34 | "b": 255 35 | }, 36 | "tab_line": { 37 | "r": 0, 38 | "g": 183, 39 | "b": 255 40 | }, 41 | "popup": { 42 | "r": 255, 43 | "g": 255, 44 | "b": 255 45 | }, 46 | "popup_text": { 47 | "r": 5, 48 | "g": 124, 49 | "b": 167 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-027.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/020.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "020", 3 | "colors": { 4 | "toolbar": { 5 | "r": 91, 6 | "g": 210, 7 | "b": 230, 8 | "a": 0.7 9 | }, 10 | "toolbar_text": { 11 | "r": 28, 12 | "g": 71, 13 | "b": 227 14 | }, 15 | "frame": { 16 | "r": 162, 17 | "g": 234, 18 | "b": 235 19 | }, 20 | "tab_background_text": { 21 | "r": 0, 22 | "g": 15, 23 | "b": 1 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 0 30 | }, 31 | "toolbar_field_text": { 32 | "r": 0, 33 | "g": 15, 34 | "b": 1 35 | }, 36 | "tab_line": { 37 | "r": 28, 38 | "g": 71, 39 | "b": 227 40 | }, 41 | "popup": { 42 | "r": 162, 43 | "g": 234, 44 | "b": 235 45 | }, 46 | "popup_text": { 47 | "r": 28, 48 | "g": 71, 49 | "b": 227 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-044.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/021.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "021", 3 | "colors": { 4 | "toolbar": { 5 | "r": 255, 6 | "g": 255, 7 | "b": 255 8 | }, 9 | "toolbar_text": { 10 | "r": 255, 11 | "g": 0, 12 | "b": 0 13 | }, 14 | "frame": { 15 | "r": 255, 16 | "g": 255, 17 | "b": 255 18 | }, 19 | "tab_background_text": { 20 | "r": 255, 21 | "g": 0, 22 | "b": 0 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 255, 31 | "g": 0, 32 | "b": 0 33 | }, 34 | "tab_line": { 35 | "r": 255, 36 | "g": 0, 37 | "b": 0 38 | }, 39 | "popup": { 40 | "r": 255, 41 | "g": 255, 42 | "b": 255 43 | }, 44 | "popup_text": { 45 | "r": 255, 46 | "g": 0, 47 | "b": 0 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": [ 52 | "./bg-011.svg" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/preset-themes/022.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "022", 3 | "colors": { 4 | "toolbar": { 5 | "r": 138, 6 | "g": 204, 7 | "b": 255, 8 | "a": 0.49 9 | }, 10 | "toolbar_text": { 11 | "r": 222, 12 | "g": 18, 13 | "b": 181 14 | }, 15 | "frame": { 16 | "r": 80, 17 | "g": 226, 18 | "b": 192 19 | }, 20 | "tab_background_text": { 21 | "r": 185, 22 | "g": 17, 23 | "b": 223 24 | }, 25 | "toolbar_field": { 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 0.36 30 | }, 31 | "toolbar_field_text": { 32 | "r": 185, 33 | "g": 17, 34 | "b": 223 35 | }, 36 | "tab_line": { 37 | "r": 222, 38 | "g": 18, 39 | "b": 181 40 | }, 41 | "popup": { 42 | "r": 228, 43 | "g": 243, 44 | "b": 255 45 | }, 46 | "popup_text": { 47 | "r": 185, 48 | "g": 17, 49 | "b": 223 50 | } 51 | }, 52 | "images": { 53 | "additional_backgrounds": [ 54 | "./bg-034.svg" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/preset-themes/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "default", 3 | "colors": { 4 | "toolbar": { 5 | "r": 225, 6 | "g": 234, 7 | "b": 239 8 | }, 9 | "toolbar_text": { 10 | "r": 248, 11 | "g": 112, 12 | "b": 140 13 | }, 14 | "frame": { 15 | "r": 142, 16 | "g": 179, 17 | "b": 201 18 | }, 19 | "tab_background_text": { 20 | "r": 255, 21 | "g": 255, 22 | "b": 255 23 | }, 24 | "toolbar_field": { 25 | "r": 255, 26 | "g": 255, 27 | "b": 255 28 | }, 29 | "toolbar_field_text": { 30 | "r": 123, 31 | "g": 127, 32 | "b": 204 33 | }, 34 | "tab_line": { 35 | "r": 248, 36 | "g": 112, 37 | "b": 140 38 | }, 39 | "popup": { 40 | "r": 255, 41 | "g": 255, 42 | "b": 255 43 | }, 44 | "popup_text": { 45 | "r": 98, 46 | "g": 102, 47 | "b": 183 48 | } 49 | }, 50 | "images": { 51 | "additional_backgrounds": ["./bg-009.svg"] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/preset-themes/hotdog.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Hotdog Stand 🌭", 3 | "colors": { 4 | "toolbar": { 5 | "r": 255, 6 | "g": 255, 7 | "b": 0 8 | }, 9 | "toolbar_text": { 10 | "r": 0, 11 | "g": 0, 12 | "b": 0 13 | }, 14 | "frame": { 15 | "r": 255, 16 | "g": 0, 17 | "b": 0 18 | }, 19 | "tab_background_text": { 20 | "r": 0, 21 | "g": 0, 22 | "b": 0 23 | }, 24 | "toolbar_field": { 25 | "r": 0, 26 | "g": 0, 27 | "b": 0 28 | }, 29 | "toolbar_field_text": { 30 | "r": 255, 31 | "g": 255, 32 | "b": 255 33 | }, 34 | "tab_line": { 35 | "r": 0, 36 | "g": 0, 37 | "b": 0 38 | }, 39 | "popup": { 40 | "r": 255, 41 | "g": 255, 42 | "b": 0 43 | }, 44 | "popup_text": { 45 | "r": 0, 46 | "g": 0, 47 | "b": 0 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/FirefoxColor/2738144bb995d5c3dd0fd604f0e9c9a746103363/src/web/favicon.ico -------------------------------------------------------------------------------- /src/web/index.html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | <%= htmlWebpackPlugin.options.title %> 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/web/index.scss: -------------------------------------------------------------------------------- 1 | @import '~fira/fira.css'; 2 | @import './lib/colors.scss'; 3 | 4 | :root { 5 | --white: #fff; 6 | --white-a96: rgba(255, 255, 255, .96); 7 | --large-unit-box-shadow: 0 5px 10px 5px rgba(0, 0, 0, .1); 8 | --large-unit-border-radius: 4px; 9 | --medium-unit-box-shadow: 0 2px 16px 0 rgba(0, 0, 0, .2); 10 | --medium-unit-border-radius: 3px; 11 | --small-unit-box-shadow: 0 2px 2px rgba(0, 0, 0, .1); 12 | --small-unit-border-radius: 2px; 13 | } 14 | 15 | html, 16 | * { 17 | box-sizing: border-box; 18 | font-family: "fira sans", sans-serif; 19 | } 20 | 21 | body { 22 | color: var(--grey-90); 23 | margin: 0; 24 | } 25 | 26 | #modal { 27 | position: absolute; 28 | pointer-events: none; 29 | top: 0; 30 | left: 0; 31 | width: 100%; 32 | height: 100%; 33 | z-index: 99999; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | justify-content: center; 38 | 39 | ul { 40 | list-style: none; 41 | padding: 0; 42 | } 43 | 44 | li { 45 | list-style: none; 46 | } 47 | 48 | > div { 49 | animation: disappear 500ms forwards; 50 | animation-delay: 2s; 51 | width: 350px; 52 | border-radius: 5px; 53 | color: #fff; 54 | padding: 1em; 55 | margin: 1em; 56 | display: flex; 57 | flex-direction: column; 58 | align-items: center; 59 | justify-content: center; 60 | background: rgba(12, 12, 12, .75); 61 | opacity: 0; 62 | transition: opacity 1s ease; 63 | 64 | &.fade-in { 65 | opacity: 1; 66 | } 67 | } 68 | } 69 | 70 | @keyframes slide-down { 71 | 0% { 72 | transform: translate3d(0, -60px, 0); 73 | opacity: 0; 74 | } 75 | 76 | 100% { 77 | transform: translate3d(0, 0, 0); 78 | opacity: 1; 79 | } 80 | } 81 | 82 | @keyframes slide-up { 83 | 0% { 84 | transform: translate3d(0, 60px, 0); 85 | opacity: 0; 86 | } 87 | 88 | 100% { 89 | transform: translate3d(0, 0, 0); 90 | opacity: 1; 91 | } 92 | } 93 | 94 | @keyframes pop-in { 95 | 0% { 96 | opacity: 0; 97 | } 98 | 99 | 80% { 100 | opacity: 1; 101 | transform: scale(1.08); 102 | } 103 | 104 | 100% { 105 | opacity: 1; 106 | transform: scale(1); 107 | } 108 | } 109 | 110 | @keyframes fade-in { 111 | from { 112 | opacity: 0; 113 | } 114 | 115 | to { 116 | opacity: 1; 117 | } 118 | } 119 | 120 | @keyframes disappear { 121 | from { 122 | opacity: 1; 123 | transform: scale(1); 124 | } 125 | 126 | to { 127 | opacity: 0; 128 | transform: scale(0); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/web/lib/_common.scss: -------------------------------------------------------------------------------- 1 | $grid: 4px; 2 | 3 | @mixin button($height: 34px, $font: 18px, $border: 2px, $minWidth: 10 * $font) { 4 | align-items: center; 5 | border-radius: $border; 6 | border: none; 7 | cursor: pointer; 8 | display: flex; 9 | font-size: $font; 10 | height: $height; 11 | justify-content: center; 12 | text-decoration: none; 13 | transition: background 50ms; 14 | min-width: $minWidth; 15 | padding: 0 $font; 16 | text-align: center; 17 | } 18 | 19 | @mixin buttonSecondary { 20 | color: var(--grey-90); 21 | background-color: rgba(12, 12, 13, .1); 22 | 23 | &:hover:not(:disabled), 24 | &:focus:not(:disabled) { 25 | background-color: rgba(12, 12, 13, .2); 26 | } 27 | 28 | &:active:not(:disabled) { 29 | background-color: rgba(12, 12, 13, .3); 30 | } 31 | 32 | &:disabled { 33 | color: var(--grey-40); 34 | cursor: default; 35 | } 36 | } 37 | 38 | @mixin buttonPrimary { 39 | color: var(--white); 40 | background-color: var(--green-60); 41 | box-shadow: 0 -3px 0 var(--grey-90-a10) inset; 42 | 43 | &:hover, 44 | &:focus { 45 | background-color: var(--green-70); 46 | } 47 | 48 | &:active { 49 | background-color: var(--green-80); 50 | } 51 | } 52 | 53 | @mixin buttonDefault { 54 | color: var(--white); 55 | background: var(--blue-50); 56 | 57 | &:hover:not(:disabled), 58 | &:focus:not(:disabled) { 59 | background-color: var(--blue-60); 60 | } 61 | 62 | &:active:not(:disabled) { 63 | background-color: var(--blue-70); 64 | } 65 | 66 | &:disabled { 67 | opacity: .5; 68 | cursor: disabled; 69 | } 70 | } 71 | 72 | @mixin unit($size: 'large') { 73 | border-radius: var(--#{$size}-unit-border-radius); 74 | box-shadow: var(--#{$size}-unit-box-shadow); 75 | background-color: var(--white-a96); 76 | } 77 | -------------------------------------------------------------------------------- /src/web/lib/_picker.scss: -------------------------------------------------------------------------------- 1 | @mixin picker-group { 2 | align-items: center; 3 | animation-duration: 500ms; 4 | animation-fill-mode: forwards; 5 | animation-name: slide-down; 6 | animation-timing-function: ease-out; 7 | background: linear-gradient(rgba(255, 255, 255, .9), var(--white)); 8 | border-radius: 2px; 9 | box-shadow: 0 2px 0 rgba(0, 0, 0, .05), 0 0 10px rgba(0, 0, 0, .1); 10 | display: flex; 11 | font-size: 16px; 12 | height: 56px; 13 | max-width: 190px; 14 | opacity: 0; 15 | padding: $grid * 2.5; 16 | position: absolute; 17 | text-shadow: 0 0 $grid * 2 #fff; 18 | transition: box-shadow 100ms, left 500ms, right 500ms, top 500ms; 19 | will-change: transform; 20 | 21 | &::after { 22 | content: ""; 23 | border-left: 12px solid transparent; 24 | border-right: 12px solid transparent; 25 | border-top: 12px solid var(--white); 26 | bottom: -11px; 27 | height: 0; 28 | position: absolute; 29 | width: 0; 30 | z-index: -1; 31 | } 32 | 33 | &::before { 34 | border-left: 14px solid transparent; 35 | border-right: 14px solid transparent; 36 | border-top: 14px solid rgba(0, 0, 0, .05); 37 | bottom: -14px; 38 | content: ""; 39 | height: 0; 40 | left: 16px; 41 | position: absolute; 42 | transition: border-top-color 100ms; 43 | width: 0; 44 | z-index: -2; 45 | } 46 | 47 | &:hover, 48 | &.selected { 49 | background: var(--white); 50 | 51 | & > span { 52 | color: var(--blue-50); 53 | } 54 | } 55 | 56 | &.selected { 57 | box-shadow: 0 0 10px rgba(0, 0, 0, .1), 0 0 0 3px var(--blue-40); 58 | z-index: 999; 59 | 60 | &::before { 61 | border-top-color: var(--blue-50); 62 | } 63 | } 64 | } 65 | 66 | @mixin picker-flipper { 67 | &::after { 68 | border-top-color: rgba(255, 255, 255, .9); 69 | bottom: auto; 70 | left: auto; 71 | right: 18px; 72 | top: -12px; 73 | transform: rotate(180deg); 74 | z-index: 1; 75 | } 76 | 77 | &::before { 78 | display: none; 79 | } 80 | 81 | &:hover { 82 | &::after { 83 | border-top-color: var(--white); 84 | } 85 | } 86 | 87 | &.selected { 88 | &::after { 89 | border-top-color: var(--white); 90 | } 91 | 92 | &::before { 93 | bottom: auto; 94 | display: inline; 95 | left: auto; 96 | right: 16px; 97 | top: -16px; 98 | transform: rotate(180deg); 99 | } 100 | } 101 | } 102 | 103 | @mixin picker-swatch { 104 | border: 2px solid var(--grey-30); 105 | border-radius: 18px; 106 | cursor: pointer; 107 | flex: 0 0 36px; 108 | height: 36px; 109 | } 110 | 111 | @mixin picker-text { 112 | padding-left: $grid * 2.5; 113 | user-select: none; 114 | cursor: pointer; 115 | } 116 | 117 | @mixin picker-small { 118 | font-size: 14px; 119 | max-width: 145px; 120 | padding: $grid * 1.5; 121 | } 122 | 123 | @mixin picker-swatch-small { 124 | border-radius: 14px; 125 | flex: 0 0 28px; 126 | height: 28px; 127 | } 128 | 129 | @mixin picker-label-small { 130 | font-size: 14px; 131 | padding-left: $grid * 1.5; 132 | } 133 | -------------------------------------------------------------------------------- /src/web/lib/colors.scss: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | /* Photon Colors CSS Variables v3.3.0 */ 6 | 7 | :root { 8 | --magenta-50: #ff1ad9; 9 | --magenta-60: #ed00b5; 10 | --magenta-70: #b5007f; 11 | --magenta-80: #7d004f; 12 | --magenta-90: #440027; 13 | 14 | --purple-30: #c069ff; 15 | --purple-40: #ad3bff; 16 | --purple-50: #9400ff; 17 | --purple-60: #8000d7; 18 | --purple-70: #6200a4; 19 | --purple-80: #440071; 20 | --purple-90: #25003e; 21 | 22 | --blue-40: #45a1ff; 23 | --blue-50: #0a84ff; 24 | --blue-50-a30: rgba(10, 132, 255, .3); 25 | --blue-60: #0060df; 26 | --blue-70: #003eaa; 27 | --blue-80: #002275; 28 | --blue-90: #000f40; 29 | 30 | --teal-50: #00feff; 31 | --teal-60: #00c8d7; 32 | --teal-70: #008ea4; 33 | --teal-80: #005a71; 34 | --teal-90: #002d3e; 35 | 36 | --green-50: #30e60b; 37 | --green-60: #12bc00; 38 | --green-70: #058b00; 39 | --green-80: #006504; 40 | --green-90: #003706; 41 | 42 | --yellow-50: #ffe900; 43 | --yellow-60: #d7b600; 44 | --yellow-70: #a47f00; 45 | --yellow-80: #715100; 46 | --yellow-90: #3e2800; 47 | 48 | --red-50: #ff0039; 49 | --red-60: #d70022; 50 | --red-70: #a4000f; 51 | --red-80: #5a0002; 52 | --red-90: #3e0200; 53 | 54 | --orange-50: #ff9400; 55 | --orange-60: #d76e00; 56 | --orange-70: #a44900; 57 | --orange-80: #712b00; 58 | --orange-90: #3e1300; 59 | 60 | --grey-10: #f9f9fa; 61 | --grey-10-a10: rgba(249, 249, 250, .1); 62 | --grey-10-a20: rgba(249, 249, 250, .2); 63 | --grey-10-a40: rgba(249, 249, 250, .4); 64 | --grey-10-a60: rgba(249, 249, 250, .6); 65 | --grey-10-a80: rgba(249, 249, 250, .8); 66 | --grey-20: #ededf0; 67 | --grey-30: #d7d7db; 68 | --grey-40: #b1b1b3; 69 | --grey-50: #737373; 70 | --grey-60: #4a4a4f; 71 | --grey-70: #38383d; 72 | --grey-80: #2a2a2e; 73 | --grey-90: #0c0c0d; 74 | --grey-90-a05: rgba(12, 12, 13, .05); 75 | --grey-90-a10: rgba(12, 12, 13, .1); 76 | --grey-90-a20: rgba(12, 12, 13, .2); 77 | --grey-90-a30: rgba(12, 12, 13, .3); 78 | --grey-90-a40: rgba(12, 12, 13, .4); 79 | --grey-90-a50: rgba(12, 12, 13, .5); 80 | --grey-90-a60: rgba(12, 12, 13, .6); 81 | --grey-90-a70: rgba(12, 12, 13, .7); 82 | --grey-90-a80: rgba(12, 12, 13, .8); 83 | --grey-90-a90: rgba(12, 12, 13, .9); 84 | 85 | --ink-70: #363959; 86 | --ink-80: #202340; 87 | --ink-90: #0f1126; 88 | 89 | --white-100: #fff; 90 | } 91 | -------------------------------------------------------------------------------- /src/web/lib/components/App/flat-firefox.svg: -------------------------------------------------------------------------------- 1 | newtab-firefox-gry -------------------------------------------------------------------------------- /src/web/lib/components/App/index.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | display: grid; 3 | grid-gap: 32px; 4 | grid-template-rows: 64px 1fr 72px; 5 | min-height: 100vh; 6 | width: 100%; 7 | } 8 | 9 | .app__main { 10 | display: grid; 11 | grid-template-columns: 20px minmax(600px, 1080px) 20px; 12 | grid-template-rows: 1fr; 13 | margin: 0 auto; 14 | position: relative; 15 | 16 | > .browser { 17 | grid-column: 2 / 3; 18 | min-height: 700px; 19 | } 20 | 21 | .app__firefox { 22 | background-color: var(--grey-10); 23 | background-image: url(./flat-firefox.svg); 24 | background-position: center center; 25 | background-repeat: no-repeat; 26 | background-size: 164px 164px; 27 | flex: 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/web/lib/components/AppBackground/AppBackground-test.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import themes from "./index"; 3 | 4 | describe("AppBackground", () => { 5 | it("Theme should exist", () => { 6 | expect(themes).to.not.be.undefined; 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/web/lib/components/AppBackground/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { colorToCSS } from "../../../../lib/themes"; 4 | 5 | import "./index.scss"; 6 | 7 | export const AppBackground = ({ theme }) => { 8 | const style = {}; 9 | 10 | // HACK: theme should not be undefined, but somehow it can be - track that down 11 | if (theme && theme.colors) { 12 | const colors = {}; 13 | Object.keys(theme.colors).forEach(key => { 14 | colors[key] = colorToCSS(theme.colors[key]); 15 | }); 16 | style.background = `linear-gradient(150deg, ${colors.toolbar} 22.5%, ${ 17 | colors.frame 18 | } 68.1%)`; 19 | } 20 | 21 | return ( 22 |
23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default AppBackground; 30 | -------------------------------------------------------------------------------- /src/web/lib/components/AppBackground/index.scss: -------------------------------------------------------------------------------- 1 | .app-background { 2 | bottom: 0; 3 | left: 0; 4 | opacity: .6; 5 | position: fixed; 6 | right: 0; 7 | top: 0; 8 | z-index: 0; 9 | 10 | &::after { 11 | background: linear-gradient(rgba(0, 0, 0, .15), transparent 25%); 12 | bottom: 0; 13 | content: ""; 14 | left: 0; 15 | position: absolute; 16 | right: 0; 17 | top: 0; 18 | z-index: 1; 19 | } 20 | 21 | .app-background__texture-1 { 22 | background: url(../../../../images/bg_texture_01.svg); 23 | background-repeat: no-repeat; 24 | background-size: 466px 644px; 25 | height: 644px; 26 | opacity: .5; 27 | position: absolute; 28 | right: 0; 29 | top: 0; 30 | width: 446px; 31 | } 32 | 33 | .app-background__texture-2 { 34 | background: url(../../../../images/bg_texture_02.svg); 35 | background-repeat: no-repeat; 36 | background-size: 236px 411px; 37 | bottom: 40px; 38 | height: 411px; 39 | left: 0; 40 | opacity: .3; 41 | position: absolute; 42 | width: 236px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/web/lib/components/AppFooter/AppFooter-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { expect } from "chai"; 3 | import { shallow, configure } from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-16"; 5 | 6 | import { AppFooter } from "./index"; 7 | 8 | configure({ adapter: new Adapter() }); 9 | 10 | describe("AppFooter", () => { 11 | const props = { 12 | hasExtension: () => {}, 13 | setDisplayLegalModal: () => {} 14 | }; 15 | 16 | it("enzyme renders without exploding", () => { 17 | expect(shallow().length).to.equal(1); 18 | }); 19 | 20 | it("should render to Mozilla logo", () => { 21 | const wrapper = shallow(); 22 | expect( 23 | wrapper 24 | .find("a.app-footer__legal-link") 25 | .at(0) 26 | .prop("href") 27 | ).to.equal("https://www.mozilla.org"); 28 | }); 29 | 30 | it("should render to app-footer legal link", () => { 31 | const wrapper = shallow(); 32 | expect( 33 | wrapper 34 | .find("a.app-footer__legal-link") 35 | .at(1) 36 | .prop("href") 37 | ).to.equal("https://www.mozilla.org/about/legal"); 38 | }); 39 | 40 | it("should show a link to app-footer social-link", () => { 41 | const wrapper = shallow(); 42 | expect(wrapper.find("a.app-footer__social-link")).to.have.property( 43 | "length", 44 | 2 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/web/lib/components/AppFooter/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactSVG from "react-svg"; 3 | 4 | import iconMoz from "./moz-logo.svg"; 5 | import iconGH from "./github-logo.svg"; 6 | import iconTwitter from "./twitter-logo.svg"; 7 | 8 | import "./index.scss"; 9 | 10 | export const AppFooter = ({ hasExtension, setDisplayLegalModal }) => { 11 | const toggleModal = (e, name) => { 12 | e.preventDefault(); 13 | setDisplayLegalModal({ display: true }); 14 | }; 15 | return ( 16 | 79 | ); 80 | }; 81 | 82 | export default AppFooter; 83 | -------------------------------------------------------------------------------- /src/web/lib/components/AppFooter/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .app-footer { 4 | display: flex; 5 | flex-direction: row; 6 | font-size: 13px; 7 | justify-content: space-between; 8 | left: 0; 9 | margin: 0 auto; 10 | padding: 0 15px; 11 | position: relative; 12 | right: 0; 13 | align-items: center; 14 | width: 100%; 15 | } 16 | 17 | .app-footer__legal { 18 | align-items: center; 19 | display: flex; 20 | flex-direction: row; 21 | max-width: 81vw; 22 | } 23 | 24 | .app-footer__legal-link { 25 | color: var(--grey-90); 26 | cursor: pointer; 27 | margin-right: 2vw; 28 | opacity: .9; 29 | text-decoration: none; 30 | white-space: nowrap; 31 | 32 | &:hover, 33 | &:focus { 34 | opacity: 1; 35 | } 36 | } 37 | 38 | .app-footer__legal-link:visited { 39 | color: var(--grey-90); 40 | } 41 | 42 | .app-footer__legal-logo { 43 | height: 32px; 44 | margin-bottom: -5px; 45 | width: 112px; 46 | } 47 | 48 | .app-footer__social { 49 | display: flex; 50 | justify-content: space-between; 51 | width: 94px; 52 | } 53 | 54 | .app-footer__social-link { 55 | opacity: 1; 56 | } 57 | 58 | .app-footer__social-logo { 59 | height: 32px; 60 | margin-bottom: -5px; 61 | width: 32px; 62 | } 63 | -------------------------------------------------------------------------------- /src/web/lib/components/AppFooter/moz-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/web/lib/components/AppFooter/twitter-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/AppHeader-test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { expect } from "chai"; 3 | import { shallow, configure } from "enzyme"; 4 | import Adapter from "enzyme-adapter-react-16"; 5 | 6 | import { AppHeader } from "./index"; 7 | 8 | configure({ adapter: new Adapter() }); 9 | 10 | describe("AppHeader", () => { 11 | const props = { 12 | theme: { 13 | colors: { 14 | tab_line: {} 15 | } 16 | }, 17 | hasExtension: () => {} 18 | }; 19 | 20 | it("enzyme renders without exploding", () => { 21 | expect(shallow().length).to.equal(1); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_export.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_forget.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_randomize.svg: -------------------------------------------------------------------------------- 1 | Created by julifrom the Noun Project -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/icon_undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/AppHeader/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .app-header { 4 | align-items: center; 5 | background: var(--grey-10); 6 | display: flex; 7 | height: 64px; 8 | justify-content: space-between; 9 | padding: 0 15px; 10 | position: relative; 11 | width: 100%; 12 | z-index: 2; 13 | 14 | .app-header__content { 15 | align-items: center; 16 | display: flex; 17 | margin: 0 auto; 18 | width: 100%; 19 | } 20 | 21 | .app-header__icon { 22 | background-image: url(../../../../images/logo.svg); 23 | background-position: center 1px; 24 | background-repeat: no-repeat; 25 | background-size: 32px auto; 26 | height: 32px; 27 | margin-right: 10px; 28 | width: 32px; 29 | } 30 | 31 | h1 { 32 | font-size: 24px; 33 | letter-spacing: -.2px; 34 | margin: 0; 35 | } 36 | 37 | .app-header__controls { 38 | display: flex; 39 | align-items: center; 40 | } 41 | 42 | .app-header__spacer { 43 | background: var(--grey-90-a10); 44 | height: 46px; 45 | width: 1px; 46 | margin: 10px; 47 | } 48 | 49 | .app-header__button { 50 | align-items: center; 51 | background: transparent; 52 | border: 0; 53 | display: flex; 54 | flex-direction: column; 55 | height: 64px; 56 | justify-content: center; 57 | padding: 0; 58 | width: 64px; 59 | 60 | .app-header__button-icon { 61 | align-items: Center; 62 | display: flex; 63 | height: 20px; 64 | margin-bottom: 1px; 65 | width: 22px; 66 | } 67 | 68 | &.disabled { 69 | pointer-events: none; 70 | 71 | .app-header__button-icon, 72 | span { 73 | opacity: .5; 74 | } 75 | } 76 | 77 | &:not(.disabled) { 78 | cursor: pointer; 79 | } 80 | 81 | &:not(.disabled):hover { 82 | background: var(--grey-90-a05); 83 | } 84 | 85 | &:not(.disabled):active { 86 | background: var(--grey-90-a10); 87 | } 88 | } 89 | } 90 | 91 | @media (max-width: 720px) { 92 | .app-header { 93 | header { 94 | display: none; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/web/lib/components/AppLoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./index.scss"; 3 | 4 | export const AppLoadingIndicator = ({ loaderQuote }) => { 5 | return ( 6 |
7 |
8 | {loaderQuote.quote} 9 |

10 | – 11 | {loaderQuote.attribution} 12 |

13 |
14 | ); 15 | }; 16 | 17 | export default AppLoadingIndicator; 18 | -------------------------------------------------------------------------------- /src/web/lib/components/AppLoadingIndicator/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../common.scss'; 2 | 3 | $spinner-shadow-width: 6px; 4 | $spinner-size: 72px; 5 | $spinner-buffer: 2px; 6 | 7 | .app-loading-indicator { 8 | align-items: center; 9 | background: var(--white); 10 | display: flex; 11 | height: 100%; 12 | justify-content: center; 13 | left: 0; 14 | position: fixed; 15 | top: 0; 16 | width: 100%; 17 | z-index: 20000; 18 | flex-direction: column; 19 | 20 | .app-loading-indicator__spinner { 21 | align-items: center; 22 | border-radius: 50%; 23 | color: white; 24 | display: flex; 25 | height: $spinner-size + $spinner-buffer; 26 | width: $spinner-size + $spinner-buffer; 27 | position: relative; 28 | overflow: hidden; 29 | 30 | &::before { 31 | content: ""; 32 | animation-duration: 8s; 33 | animation-iteration-count: infinite; 34 | animation-name: rainbow; 35 | animation-timing-function: ease-in-out; 36 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(0, 50, 50); 37 | width: $spinner-size; 38 | height: $spinner-size; 39 | border-radius: 50%; 40 | margin: .5 * $spinner-buffer; 41 | } 42 | 43 | &::after { 44 | content: ""; 45 | box-sizing: border-box; 46 | width: $spinner-size; 47 | height: $spinner-size; 48 | animation: rotate linear 1.88s infinite; 49 | transform-origin: left top; 50 | background: rgba(255, 255, 255, .7); 51 | position: absolute; 52 | top: (.5 * $spinner-size) + (.5 * $spinner-buffer); 53 | left: (.5 * $spinner-size) + (.5 * $spinner-buffer); 54 | } 55 | } 56 | 57 | .app-loading-indicator__quote { 58 | font-size: 24px; 59 | line-height: 32px; 60 | margin: 40px 0 20px; 61 | max-width: 360px; 62 | text-align: center; 63 | } 64 | 65 | .app-loading-indicator__attribution { 66 | font-style: oblique; 67 | margin: 0; 68 | max-width: 360px; 69 | } 70 | } 71 | 72 | @keyframes rotate { 73 | 0% { 74 | transform: rotate(0); 75 | } 76 | 77 | 100% { 78 | transform: rotate(360deg); 79 | } 80 | } 81 | 82 | @keyframes rainbow { 83 | 0% { 84 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(0, 50, 50); 85 | } 86 | 87 | 16% { 88 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(60, 50, 50); 89 | } 90 | 91 | 32% { 92 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(120, 50, 50); 93 | } 94 | 95 | 48% { 96 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(180, 50, 50); 97 | } 98 | 99 | 64% { 100 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(240, 50, 50); 101 | } 102 | 103 | 80% { 104 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(300, 50, 50); 105 | } 106 | 107 | 96% { 108 | box-shadow: 0 0 0 $spinner-shadow-width inset hsl(360, 50, 50); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/web/lib/components/Banner/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import classnames from "classnames"; 3 | 4 | import { DOWNLOAD_FIREFOX_URL } from "../../../../lib/constants"; 5 | 6 | import "./index.scss"; 7 | 8 | export const Banner = ({ isFirefox, addonUrl }) => { 9 | return ( 10 |
11 |
12 | {isFirefox && ( 13 | 14 |

Build Beautiful Firefox Themes

15 | 16 | Get Firefox Color 17 | 18 |
19 | )} 20 | {!isFirefox && ( 21 | 22 |

Browse in Style

23 |

Create unique Firefox themes with just a few clicks

24 | 25 | Download Firefox 26 | 27 |
28 | )} 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Banner; 35 | -------------------------------------------------------------------------------- /src/web/lib/components/Banner/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .banner { 4 | flex: 1; 5 | } 6 | 7 | .banner__content { 8 | align-items: center; 9 | animation: fade-in 500ms forwards; 10 | animation-delay: 250ms; 11 | display: flex; 12 | flex-direction: column; 13 | height: 100%; 14 | justify-content: center; 15 | opacity: 0; 16 | width: 100%; 17 | z-index: 999; 18 | padding: 32px; 19 | 20 | h2 { 21 | align-items: center; 22 | display: flex; 23 | font-size: 40px; 24 | font-weight: 600; 25 | justify-content: center; 26 | width: 100%; 27 | line-height: 40px; 28 | margin: 0 0 32px; 29 | text-align: center; 30 | } 31 | 32 | h3 { 33 | font-size: 16px; 34 | font-weight: normal; 35 | margin: 0 0 16px; 36 | } 37 | 38 | p { 39 | align-items: center; 40 | color: var(--grey-50); 41 | display: flex; 42 | font-size: 12px; 43 | line-height: 12px; 44 | margin: 12px 0 0; 45 | } 46 | 47 | .banner__button { 48 | @include buttonPrimary; 49 | @include button(64px, 24px, 32px, 300px); 50 | 51 | background-image: url("./firefox.svg"); 52 | background-position: 12px center; 53 | background-repeat: no-repeat; 54 | background-size: auto 44px; 55 | margin: 0 auto; 56 | padding-left: 64px; 57 | } 58 | } 59 | 60 | @media (max-height: 720px), (max-width: 640px) { 61 | .banner__content { 62 | padding: 20px; 63 | 64 | h2 { 65 | font-size: 32px; 66 | line-height: 32px; 67 | margin: 0 0 20px; 68 | text-align: center; 69 | 70 | .banner--ff-ad & { 71 | margin: 0; 72 | } 73 | } 74 | 75 | h3 { 76 | margin: 8px 0 24px; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/web/lib/components/Browser/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BrowserChrome from "../BrowserChrome"; 3 | import BrowserTabs from "../BrowserTabs"; 4 | import BrowserTools from "../BrowserTools"; 5 | import BrowserPopup from "../BrowserPopup"; 6 | 7 | import { colorToCSS } from "../../../../lib/themes"; 8 | import { bgImages } from "../../../../lib/assets"; 9 | 10 | import "./index.scss"; 11 | 12 | const Browser = ({ 13 | theme, 14 | themeHasCustomBackgrounds = null, 15 | customImages = [], 16 | size = "small", 17 | selectedColor = null, 18 | children = null, 19 | onClick: onClickBrowser = null, 20 | showPopup = true 21 | }) => { 22 | const clickBrowser = e => { 23 | if (onClickBrowser) { 24 | onClickBrowser(e); 25 | e.stopPropagation(); 26 | } 27 | return false; 28 | }; 29 | 30 | // get all the colors to pass into various browser bits 31 | const colors = {}; 32 | Object.keys(theme.colors).forEach(key => { 33 | colors[key] = colorToCSS(theme.colors[key]); 34 | }); 35 | 36 | // now do the backgrounds 37 | const headerBackground = theme.images.additional_backgrounds[0]; 38 | const headerBackgroundImage = bgImages.keys().includes(headerBackground) 39 | ? `url(${bgImages(headerBackground)})` 40 | : ""; 41 | 42 | const selectSettings = { 43 | transition: "outline 250ms", 44 | active: "3px dashed red", 45 | inactive: "3px dashed transparent" 46 | }; 47 | 48 | return ( 49 |
50 | 60 |
69 | 77 | 78 |
79 | {showPopup && ( 80 | 81 | )} 82 |
83 |
{children}
84 |
85 | ); 86 | }; 87 | 88 | export default Browser; 89 | -------------------------------------------------------------------------------- /src/web/lib/components/Browser/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .browser { 4 | background: var(--white-100); 5 | position: relative; 6 | border-radius: 3px; 7 | display: flex; 8 | flex-direction: column; 9 | flex: 1; 10 | } 11 | 12 | .browser--large { 13 | @include unit("large"); 14 | 15 | .browser__content { 16 | background-color: var(--grey-10); 17 | display: flex; 18 | flex-direction: column; 19 | height: 100%; 20 | justify-content: space-between; 21 | position: relative; 22 | z-index: 0; 23 | } 24 | } 25 | 26 | .browser--medium { 27 | @include unit("medium"); 28 | } 29 | 30 | .browser--small { 31 | @include unit("small"); 32 | 33 | border: 1px solid var(--grey-20); 34 | 35 | .browser__content { 36 | padding: $grid * 1.5; 37 | flex: 1; 38 | border-top: 1px solid var(--grey-20); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserChrome/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./index.scss"; 4 | 5 | const BrowserChrome = ({ 6 | colors, 7 | headerBackgroundImage, 8 | customImages, 9 | children, 10 | selectSettings, 11 | themeHasCustomBackgrounds, 12 | selectedColor = false 13 | }) => { 14 | return ( 15 |
24 | {customImages.map((image, index) => { 25 | return ( 26 |
39 | ); 40 | })} 41 | {children} 42 |
43 | ); 44 | }; 45 | 46 | export default BrowserChrome; 47 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserChrome/index.scss: -------------------------------------------------------------------------------- 1 | .browser-chrome { 2 | border-radius: 3px 3px 0 0; 3 | position: relative; 4 | z-index: 1; 5 | } 6 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserPopup/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./index.scss"; 4 | 5 | const BrowserPopup = ({ 6 | colors, 7 | size = "small", 8 | selectSettings, 9 | selectedColor 10 | }) => { 11 | const items = ["item one", "item two"]; 12 | if (size === "large") { 13 | items.push("item three"); 14 | } 15 | return ( 16 |
17 |
27 |
33 |
    44 | {items.map((item, index) => { 45 | return ( 46 |
  • 57 |
    63 |
  • 64 | ); 65 | })} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default BrowserPopup; 72 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserPopup/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .browser-popup { 4 | position: absolute; 5 | z-index: 11; 6 | 7 | .browser-popup__caret-shadow { 8 | transform: rotate(45deg); 9 | position: absolute; 10 | z-index: -1; 11 | } 12 | 13 | .browser-popup__caret { 14 | transform: rotate(45deg); 15 | position: absolute; 16 | z-index: 1; 17 | } 18 | 19 | .browser-popup__inner { 20 | position: relative; 21 | margin: 0; 22 | list-style: none; 23 | } 24 | 25 | &.browser-popup--large, 26 | &.browser-popup--medium { 27 | top: 106px; 28 | right: -8px; 29 | 30 | .browser-popup__inner { 31 | @include unit("medium"); 32 | 33 | border-radius: 4px; 34 | width: 240px; 35 | padding: 24px 18px; 36 | 37 | li { 38 | margin: 16px 0; 39 | 40 | &:first-child { 41 | width: 90%; 42 | margin-top: 0; 43 | } 44 | 45 | &:last-child { 46 | width: 95%; 47 | margin-bottom: 0; 48 | } 49 | } 50 | } 51 | 52 | .browser-popup__caret-shadow { 53 | width: 22px; 54 | height: 22px; 55 | right: 26px; 56 | top: -5px; 57 | clip-path: polygon(-15px -25px, -15px 28px, 40px -26px); 58 | box-shadow: var(--medium-unit-box-shadow); 59 | } 60 | 61 | .browser-popup__caret { 62 | width: 20px; 63 | height: 20px; 64 | right: 27px; 65 | top: -5px; 66 | clip-path: polygon(-15px -25px, -15px 28px, 40px -26px); 67 | } 68 | 69 | .browser-popup__item { 70 | height: 10px; 71 | border-radius: 5px; 72 | } 73 | } 74 | 75 | &.browser-popup--medium { 76 | top: 90px; 77 | } 78 | 79 | &.browser-popup--small { 80 | top: 44px; 81 | right: 0; 82 | 83 | .browser-popup__inner { 84 | @include unit("small"); 85 | 86 | border: 1px solid var(--grey-90-a05); 87 | border-radius: 2px; 88 | padding: 6px 8px; 89 | width: 80px; 90 | } 91 | 92 | .browser-popup__caret-shadow { 93 | width: 12px; 94 | height: 12px; 95 | right: 6px; 96 | top: -5px; 97 | } 98 | 99 | .browser-popup__caret { 100 | width: 10px; 101 | height: 10px; 102 | right: 7px; 103 | top: -5px; 104 | } 105 | 106 | .browser-popup__item { 107 | height: 4px; 108 | margin: 2px 0; 109 | border-radius: 2px; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserTabs/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .browser-tabs { 4 | width: 100%; 5 | position: relative; 6 | z-index: 10; 7 | } 8 | 9 | .browser-tabs__inner { 10 | display: grid; 11 | list-style: none; 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | .browser-tabs__tab { 17 | -moz-user-select: none; 18 | display: flex; 19 | justify-content: center; 20 | margin: 0; 21 | padding: 0; 22 | position: relative; 23 | text-align: center; 24 | } 25 | 26 | .browser-tabs__tab-inner { 27 | flex: 1; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | position: relative; 32 | } 33 | 34 | .browser-tabs__tab-line { 35 | position: absolute; 36 | top: 0; 37 | left: 0; 38 | right: 0; 39 | height: 3px; 40 | opacity: 0; 41 | transform: scaleX(0); 42 | transition: transform 75ms, opacity 200ms; 43 | transform-origin: center top; 44 | transition-timing-function: cubic-bezier(.17, .67, .83, .67); 45 | } 46 | 47 | .browser-tabs--large { 48 | height: 54px; 49 | 50 | > .browser-tabs__inner { 51 | grid-template-columns: repeat(3, minmax(180px, 240px)); 52 | grid-template-rows: 54px; 53 | padding: 0 $grid * 16; 54 | } 55 | 56 | .browser-tabs__title { 57 | border-radius: 5px; 58 | height: $grid * 2.5; 59 | width: $grid * 32; 60 | } 61 | } 62 | 63 | .browser-tabs--medium { 64 | height: $grid * 10; 65 | padding: 0 $grid * 9; 66 | border-radius: 4px 4px 0 0; 67 | 68 | > .browser-tabs__inner { 69 | grid-template-columns: repeat(3, 1fr); 70 | grid-template-rows: $grid * 10; 71 | } 72 | 73 | .browser-tabs__tab { 74 | align-items: center; 75 | } 76 | 77 | .browser-tabs__title { 78 | border-radius: 4px; 79 | height: $grid * 2; 80 | width: $grid * 20; 81 | } 82 | } 83 | 84 | .browser-tabs--small { 85 | height: $grid * 5; 86 | padding: 0 $grid * 4; 87 | 88 | > .browser-tabs__inner { 89 | grid-template-columns: repeat(3, 1fr); 90 | grid-template-rows: $grid * 5; 91 | } 92 | 93 | .browser-tabs__tab { 94 | align-items: center; 95 | } 96 | 97 | .browser-tabs__title { 98 | margin-top: 1px; 99 | border-radius: 2px; 100 | height: $grid; 101 | width: $grid * 8; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserTools/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactSVG from "react-svg"; 3 | 4 | import { buttonImages } from "../../../../lib/assets"; 5 | 6 | import "./index.scss"; 7 | 8 | export const BrowserTools = ({ 9 | colors, 10 | size = "small", 11 | selectSettings, 12 | selectedColor = null 13 | }) => { 14 | const Button = ({ name, asset = false, colorName = "toolbar_text" }) => ( 15 | 25 | {asset && ( 26 | 30 | )} 31 | {!asset && ( 32 |
36 | )} 37 | 38 | ); 39 | 40 | return ( 41 |
42 |
53 |
81 |
83 |
84 | ); 85 | }; 86 | 87 | export default BrowserTools; 88 | -------------------------------------------------------------------------------- /src/web/lib/components/BrowserTools/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .browser-tools { 4 | position: relative; 5 | width: 100%; 6 | z-index: 10; 7 | 8 | .browser-tools__inner { 9 | align-items: center; 10 | display: flex; 11 | } 12 | 13 | .browser-tools__field { 14 | align-items: center; 15 | display: flex; 16 | flex-grow: 1; 17 | 18 | .browser-tools__button { 19 | padding: 0; 20 | } 21 | } 22 | 23 | .browser-tools__field-text { 24 | flex: 1; 25 | } 26 | 27 | .browser-tools__button { 28 | align-items: center; 29 | display: flex; 30 | flex-grow: 0; 31 | margin: 0; 32 | 33 | > div > div { 34 | align-items: center; 35 | display: flex; 36 | justify-content: center; 37 | } 38 | } 39 | 40 | .browser-tools__button-inner { 41 | border-radius: 50%; 42 | } 43 | } 44 | 45 | .browser-tools--large { 46 | > .browser-tools__inner { 47 | height: $grid * 15; 48 | padding: 0 $grid * 2.5; 49 | } 50 | 51 | .browser-tools__field { 52 | border-radius: 3px; 53 | font-size: $grid * 5; 54 | height: $grid * 9; 55 | margin: 0 $grid * 2.5; 56 | 57 | .browser-tools__field-text { 58 | border-radius: 5px; 59 | height: 10px; 60 | margin: 0 0 0 $grid * 10; 61 | } 62 | 63 | .browser-tools__button { 64 | flex: 0 0 18px; 65 | height: 18px; 66 | margin: 16px; 67 | } 68 | 69 | .browser-tools__button-inner { 70 | flex: 0 0 18px; 71 | height: 18px; 72 | } 73 | } 74 | 75 | .browser-tools__button { 76 | padding: $grid * 2; 77 | } 78 | 79 | .browser-tools__button-inner { 80 | height: 24px; 81 | width: 24px; 82 | } 83 | 84 | img, 85 | svg { 86 | height: 24px; 87 | width: 24px; 88 | } 89 | } 90 | 91 | .browser-tools--medium { 92 | .browser-tools__inner { 93 | height: $grid * 14; 94 | padding: 0 $grid * 2.5; 95 | } 96 | 97 | .browser-tools__field { 98 | border-radius: $grid; 99 | height: $grid * 8; 100 | margin: 0 $grid * 2.5; 101 | 102 | .browser-tools__field-text { 103 | border-radius: $grid; 104 | height: $grid * 2.5; 105 | margin: 0 $grid * 18 0 $grid * 5; 106 | } 107 | } 108 | 109 | .browser-tools__button { 110 | padding: 0 $grid * 2.5; 111 | } 112 | 113 | .browser-tools__button-inner { 114 | height: $grid * 5; 115 | width: $grid * 5; 116 | } 117 | } 118 | 119 | .browser-tools--small { 120 | .browser-tools__inner { 121 | height: $grid * 7.5; 122 | padding: 0 $grid 0 $grid * .5; 123 | } 124 | 125 | .browser-tools__field { 126 | border-radius: $grid * .5; 127 | height: $grid * 3.5; 128 | margin: 0 $grid; 129 | 130 | .browser-tools__field-text { 131 | border-radius: $grid * .5; 132 | height: $grid; 133 | margin: 0 $grid * 7.5 0 $grid * 2; 134 | } 135 | } 136 | 137 | .browser-tools__button { 138 | padding: 0 $grid; 139 | } 140 | 141 | .browser-tools__button-inner { 142 | height: $grid * 2; 143 | width: $grid * 2; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/web/lib/components/ClearImageModal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Modal from "../Modal"; 3 | 4 | import "./index.scss"; 5 | 6 | export default class ClearImageModal extends React.Component { 7 | render() { 8 | return ( 9 | 10 |

11 | Deleting this image will remove it from any saved themes you have. Do 12 | you want to proceed? 13 |

14 |
15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/web/lib/components/ClearImageModal/index.scss: -------------------------------------------------------------------------------- 1 | .modal-wrapper--clear-image { 2 | position: absolute; 3 | right: -10px; 4 | top: -20px; 5 | 6 | .modal--display { 7 | border-radius: 4px; 8 | } 9 | 10 | .modal__content { 11 | animation: none; 12 | } 13 | 14 | .modal { 15 | position: relative; 16 | right: 100px; 17 | z-index: 99999; 18 | top: 150px; 19 | } 20 | 21 | .modal__toggle { 22 | display: none; 23 | } 24 | 25 | .modal__buttons { 26 | display: flex; 27 | justify-content: space-around; 28 | } 29 | 30 | button { 31 | background: var(--blue-50); 32 | height: 40px; 33 | border: none; 34 | font-size: 14px; 35 | color: var(--white); 36 | cursor: pointer; 37 | padding: 0 10px; 38 | min-width: 90px; 39 | border-radius: 2px; 40 | margin: 20px 10px 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/web/lib/components/GeneratorButtons/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | generateComplementaryTheme, 4 | generateRandomTheme 5 | } from "../../../../lib/generators"; 6 | 7 | import "./index.scss"; 8 | 9 | export const GeneratorButtons = ({ setTheme, theme }) => { 10 | const handleComplementClick = () => { 11 | setTheme({ theme: generateComplementaryTheme() }); 12 | }; 13 | const handleRandomClick = () => { 14 | setTheme({ theme: generateRandomTheme() }); 15 | }; 16 | 17 | return ( 18 |
19 |

Generate a theme

20 | 27 | 34 |
35 | ); 36 | }; 37 | 38 | export default GeneratorButtons; 39 | -------------------------------------------------------------------------------- /src/web/lib/components/GeneratorButtons/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../common.scss'; 2 | 3 | .generator-buttons { 4 | h2 { 5 | margin-top: 0; 6 | } 7 | 8 | .generator-buttons__button { 9 | @include button; 10 | @include buttonDefault; 11 | 12 | margin: 0 0 20px 0; 13 | width: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/web/lib/components/Mobile/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./index.scss"; 4 | 5 | export const Mobile = () => ( 6 |
7 |
8 |
9 |

Firefox Color

10 |

11 | Firefox Color is an add-on that lets you build heartbreakingly beautiful 12 | themes for Firefox. 13 |

14 |

15 | Sadly, it’s only available for Firefox on Mac, Windows and Linux. 16 |

17 |
18 |
19 | ); 20 | 21 | export default Mobile; 22 | -------------------------------------------------------------------------------- /src/web/lib/components/Mobile/index.scss: -------------------------------------------------------------------------------- 1 | .mobile { 2 | align-items: center; 3 | background: linear-gradient(135deg, var(--teal-60) 0%, var(--blue-50) 100%); 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | min-height: 100vh; 8 | } 9 | 10 | .mobile__content { 11 | margin: 0 auto; 12 | max-width: 320px; 13 | text-align: center; 14 | width: 86%; 15 | 16 | h1 { 17 | font-weight: 300; 18 | } 19 | 20 | p { 21 | font-size: 1.1rem; 22 | line-height: 1.66rem; 23 | } 24 | 25 | h1, 26 | p { 27 | color: var(--white-100); 28 | margin: 0 0 24px; 29 | } 30 | 31 | a { 32 | align-items: center; 33 | border-radius: 30px; 34 | border: 2px solid var(--white-100); 35 | color: var(--white-100); 36 | display: flex; 37 | font-size: 1.1rem; 38 | height: 60px; 39 | justify-content: center; 40 | line-height: 1.66rem; 41 | margin: 0 auto; 42 | max-width: 240px; 43 | text-decoration: none; 44 | } 45 | } 46 | 47 | .mobile__logo { 48 | background-image: url(../../../../images/logo-white.svg); 49 | background-position: center center; 50 | background-repeat: no-repeat; 51 | background-size: 76px auto; 52 | height: 76px; 53 | margin: 0 auto 12px; 54 | width: 76px; 55 | } 56 | -------------------------------------------------------------------------------- /src/web/lib/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import classNames from "classnames"; 3 | 4 | import iconClose from "../../../../images/close-16.svg"; 5 | import "./index.scss"; 6 | 7 | export const Modal = ({ toggleModal, displayModal, children }) => { 8 | const handleToggle = e => { 9 | e.stopPropagation(); 10 | toggleModal({ display: false }); 11 | }; 12 | 13 | useEffect(() => { 14 | document.addEventListener("keydown", handleKeyPress); 15 | 16 | return () => { 17 | document.removeEventListener("keydown", handleKeyPress); 18 | }; 19 | }, []); 20 | 21 | const handleKeyPress = event => { 22 | if (event.keyCode === 27) { 23 | toggleModal({ display: false }); 24 | } 25 | }; 26 | 27 | return ( 28 |
32 |
event.stopPropagation()} 35 | > 36 | 39 | {children} 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Modal; 46 | -------------------------------------------------------------------------------- /src/web/lib/components/Modal/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../common.scss'; 2 | 3 | .modal { 4 | align-items: center; 5 | background: var(--grey-90-a60); 6 | bottom: 0; 7 | display: none; 8 | justify-content: center; 9 | left: 0; 10 | position: fixed; 11 | right: 0; 12 | top: 0; 13 | z-index: 999; 14 | } 15 | 16 | .modal--display { 17 | display: flex; 18 | } 19 | 20 | .modal__content { 21 | @include unit; 22 | 23 | align-items: center; 24 | animation: translate-in 250ms forwards ease-out; 25 | background: var(--white-100); 26 | box-shadow: 0 10px 20px var(--grey-90-a20); 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: space-around; 30 | line-height: 1.5rem; 31 | min-height: 200px; 32 | padding: 8 * $grid 10 * $grid; 33 | position: relative; 34 | text-align: center; 35 | width: 400px; 36 | will-change: transform; 37 | 38 | h2, 39 | p { 40 | margin: 0; 41 | } 42 | 43 | a:not(.modal__button):not(:visited) { 44 | color: var(--blue-50); 45 | } 46 | } 47 | 48 | .modal__toggle { 49 | @include buttonSecondary; 50 | 51 | align-items: center; 52 | border-radius: 3px; 53 | border: 0; 54 | cursor: pointer; 55 | display: flex; 56 | height: 40px; 57 | justify-content: center; 58 | position: absolute; 59 | right: $grid * 3; 60 | top: $grid * 3; 61 | width: 40px; 62 | } 63 | 64 | .modal__form { 65 | display: flex; 66 | flex-direction: column; 67 | align-items: center; 68 | 69 | input { 70 | padding: 8px; 71 | border: 1px solid var(--grey-90-a20); 72 | width: 320px; 73 | border-radius: 3px; 74 | font-size: 16px; 75 | margin-top: 3 * $grid; 76 | } 77 | 78 | &:focus { 79 | border: 1px solid var(--blue-50); 80 | } 81 | } 82 | 83 | .modal__buttons { 84 | display: flex; 85 | justify-content: space-around; 86 | } 87 | 88 | .modal__button { 89 | @include button(40px, 20px, 2px, 150px); 90 | @include buttonDefault; 91 | 92 | margin: 0 $grid * 2; 93 | 94 | &:disabled { 95 | opacity: .7; 96 | cursor: default; 97 | } 98 | } 99 | 100 | @keyframes translate-in { 101 | 0% { 102 | opacity: 0; 103 | transform: translate3d(0, 120px, 0); 104 | } 105 | 106 | 100% { 107 | opacity: 1; 108 | transform: translate3d(0, 0, 0); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/web/lib/components/Onboarding/LogoIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LogoIcon = props => ( 4 | 11 | Artboard 12 | 13 | 18 | 19 | 20 | 21 | 30 | 35 | 36 | 37 | 38 | ); 39 | 40 | export default LogoIcon; 41 | -------------------------------------------------------------------------------- /src/web/lib/components/Onboarding/SparklesIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SparklesIcon = props => ( 4 | 12 | 19 | 23 | 24 | 25 | 26 | 27 | 32 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | 51 | export default SparklesIcon; 52 | -------------------------------------------------------------------------------- /src/web/lib/components/Onboarding/index.scss: -------------------------------------------------------------------------------- 1 | @import '../../common.scss'; 2 | 3 | .onboarding { 4 | align-items: center; 5 | opacity: 0; 6 | animation: fade-in forwards 500ms; 7 | animation-delay: 250ms; 8 | background: rgba(0, 0, 0, .5); 9 | bottom: 0; 10 | display: none; 11 | flex-direction: column; 12 | justify-content: center; 13 | left: 0; 14 | position: fixed; 15 | right: 0; 16 | top: 0; 17 | z-index: 999; 18 | } 19 | 20 | .onboarding--display { 21 | display: flex; 22 | } 23 | 24 | .onboarding__panels { 25 | align-items: center; 26 | border-radius: var(--large-unit-border-radius); 27 | box-shadow: var(--large-unit-box-shadow); 28 | display: flex; 29 | flex-direction: column; 30 | height: 320px; 31 | justify-content: center; 32 | position: relative; 33 | transition: background 250ms; 34 | width: 540px; 35 | z-index: 1; 36 | 37 | button { 38 | background: transparent; 39 | border-radius: 3px; 40 | border: 0; 41 | border: 1px solid white; 42 | bottom: 10px; 43 | color: white; 44 | cursor: pointer; 45 | font-size: 16px; 46 | padding: 10px; 47 | position: absolute; 48 | right: 10px; 49 | transition: background 100ms; 50 | width: 100px; 51 | 52 | &:hover { 53 | background: rgba(255, 255, 255, .1); 54 | } 55 | 56 | &:active { 57 | background: rgba(255, 255, 255, .2); 58 | } 59 | } 60 | } 61 | 62 | .onboarding__content { 63 | align-items: center; 64 | color: white; 65 | display: flex; 66 | flex-direction: column; 67 | font-size: 24px; 68 | font-weight: bold; 69 | justify-content: center; 70 | line-height: 32px; 71 | padding: 20px; 72 | text-align: center; 73 | overflow: hidden; 74 | } 75 | 76 | @keyframes animatedBackground { 77 | 0% { transform: translateX(182px); } 78 | 100% { transform: translateX(0); } 79 | } 80 | 81 | .stuff { 82 | animation: animatedBackground 500ms; 83 | margin-bottom: 12px; 84 | 85 | svg { 86 | &:first-child { 87 | transform: translate(-22px, -70px); 88 | } 89 | 90 | &:last-child { 91 | transform: translate(30px, 6px); 92 | } 93 | } 94 | } 95 | 96 | .onboarding__icon { 97 | background-image: url(../../../../images/onboarding.png); 98 | background-repeat: no-repeat; 99 | background-size: 1400px 110px; 100 | height: 110px; 101 | margin: -24px auto 24px; 102 | overflow: hidden; 103 | transition: background-position 500ms; 104 | width: 280px; 105 | 106 | &.onboarding__icon--close { 107 | background-image: none; 108 | height: 0; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/web/lib/components/PaginatedThemeSelector/arrow_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/PaginatedThemeSelector/arrow_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/PaginatedThemeSelector/close.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/web/lib/components/PresetThemeSelector/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import PaginatedThemeSelector from "../PaginatedThemeSelector"; 4 | 5 | import { presetThemes } from "../../../../lib/themes"; 6 | 7 | export const PresetThemeSelector = ({ 8 | setTheme, 9 | presetThemesPage, 10 | setPresetThemesPage 11 | }) => { 12 | const sortedPresetThemes = presetThemes.map(item => [ 13 | item.idx, 14 | { theme: item } 15 | ]); 16 | 17 | return ( 18 | { 24 | setTheme({ theme }); 25 | }} 26 | perPage={9} 27 | currentPage={presetThemesPage} 28 | setCurrentPage={setPresetThemesPage} 29 | /> 30 | ); 31 | }; 32 | 33 | export default PresetThemeSelector; 34 | -------------------------------------------------------------------------------- /src/web/lib/components/SavedThemeSelector/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import PaginatedThemeSelector from "../PaginatedThemeSelector"; 4 | 5 | export const SavedThemeSelector = ({ 6 | setTheme, 7 | savedThemes, 8 | savedThemesPage, 9 | setSavedThemesPage, 10 | themeCustomImages, 11 | storage, 12 | clearCustomBackground 13 | }) => { 14 | const { themeStorage } = storage; 15 | const sortedSavedThemes = Object.entries(savedThemes).sort( 16 | ([, aData], [, bData]) => bData.modified - aData.modified 17 | ); 18 | 19 | const deleteTheme = key => { 20 | const themeBeingDeleted = themeStorage.get(key); 21 | themeStorage.delete(key); 22 | const { images } = themeBeingDeleted.theme; 23 | 24 | // Remove current images from the display and if it's not present in other themes, from local storage . 25 | if ( 26 | images && 27 | images.custom_backgrounds && 28 | images.custom_backgrounds.length 29 | ) { 30 | const entries = Object.entries(savedThemes); 31 | let themes = entries.filter(([entry]) => entry !== key); 32 | const values = Object.values(themes); 33 | 34 | const savedThemeImages = values 35 | .map(([_, item]) => item.theme.images.custom_backgrounds) 36 | .reduce((acc, item) => { 37 | (item || []).forEach((bg, i) => { 38 | acc.push(bg.name); 39 | }); 40 | return acc; 41 | }, []); 42 | 43 | const { custom_backgrounds: customImages } = images; 44 | const currentImages = customImages.map(({ name }) => name); 45 | 46 | for (let index = currentImages.length - 1; index >= 0; index--) { 47 | if (!savedThemeImages.includes(currentImages[index])) { 48 | clearCustomBackground({ index }); 49 | } 50 | } 51 | } else { 52 | // If no current images are in preview, then remove unused background images 53 | // from local storage. 54 | clearCustomBackground({ index: 0 }); 55 | } 56 | }; 57 | 58 | return ( 59 | setTheme({ theme })} 65 | onDelete={key => deleteTheme(key)} 66 | perPage={9} 67 | currentPage={savedThemesPage} 68 | setCurrentPage={setSavedThemesPage} 69 | images={themeCustomImages} 70 | /> 71 | ); 72 | }; 73 | 74 | export default SavedThemeSelector; 75 | -------------------------------------------------------------------------------- /src/web/lib/components/SharedThemeDialog/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Browser from "../Browser"; 3 | import "./index.scss"; 4 | 5 | export const SharedThemeDialog = ({ 6 | pendingTheme, 7 | setTheme, 8 | clearPendingTheme, 9 | previewTheme 10 | }) => { 11 | const onApply = () => { 12 | setTheme({ theme: pendingTheme }); 13 | clearPendingTheme(); 14 | }; 15 | const onSkip = () => { 16 | clearPendingTheme(); 17 | }; 18 | const onClickBackdrop = ev => { 19 | if (ev.target && ev.target.className === "shared-theme-dialog-wrapper") { 20 | onSkip(); 21 | } 22 | }; 23 | const onHover = mouseEnter => { 24 | if (mouseEnter) { 25 | previewTheme({ theme: pendingTheme }); 26 | } else { 27 | previewTheme({}); 28 | } 29 | }; 30 | return ( 31 |
32 |
33 |
onHover(true)} 36 | onMouseLeave={() => onHover(false)} 37 | > 38 | 39 |
40 |
41 |

Do you want to apply this custom theme?

42 |
43 | 46 | 49 |
50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default SharedThemeDialog; 57 | -------------------------------------------------------------------------------- /src/web/lib/components/SharedThemeDialog/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .shared-theme-dialog-wrapper { 4 | align-items: center; 5 | animation-duration: 250ms; 6 | animation-fill-mode: forwards; 7 | animation-name: fade-in; 8 | background: rgba(12, 12, 13, .6); 9 | display: flex; 10 | height: 100%; 11 | justify-content: center; 12 | left: 0; 13 | opacity: 0; 14 | position: fixed; 15 | top: 0; 16 | width: 100%; 17 | z-index: 10000; 18 | 19 | .shared-theme-dialog { 20 | @include unit; 21 | 22 | animation-delay: 500ms; 23 | animation-duration: 250ms; 24 | animation-fill-mode: forwards; 25 | animation-name: pop-in; 26 | background: #30324f; 27 | border-radius: 6px; 28 | height: 437px; 29 | opacity: 0; 30 | overflow: hidden; 31 | position: relative; 32 | width: 600px; 33 | 34 | .preview { 35 | display: flex; 36 | justify-content: center; 37 | background-image: url(./stars-bg.svg); 38 | background-repeat: no-repeat; 39 | border-radius: 6px 6px 0 0; 40 | height: 282px; 41 | left: 21px; 42 | position: absolute; 43 | top: 50px; 44 | width: calc(100% - 42px); 45 | } 46 | 47 | .browser { 48 | width: 80%; 49 | } 50 | 51 | .options { 52 | align-items: center; 53 | background: var(--white); 54 | bottom: 0; 55 | display: flex; 56 | flex-direction: column; 57 | height: 155px; 58 | justify-content: center; 59 | position: absolute; 60 | width: 100%; 61 | z-index: 20000; 62 | box-shadow: 0 0 20px rgba(12, 12, 13, .3); 63 | 64 | .buttons { 65 | display: flex; 66 | flex-direction: row; 67 | justify-content: center; 68 | width: 100%; 69 | 70 | button { 71 | @include button(34px, 16px); 72 | @include buttonDefault; 73 | 74 | margin: 10px; 75 | } 76 | 77 | button.skip { 78 | @include button(34px, 16px); 79 | @include buttonSecondary; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/web/lib/components/SharedThemeDialog/stars-bg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/StorageSpaceInformation/StorageIcon.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const StorageIcon = props => ( 4 | 12 | 13 | 14 | ); 15 | 16 | export default StorageIcon; 17 | -------------------------------------------------------------------------------- /src/web/lib/components/StorageSpaceInformation/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import StorageIcon from "./StorageIcon"; 4 | import { actions } from "../../../../lib/store"; 5 | 6 | import "./index.scss"; 7 | 8 | export const STORAGE_ERROR_MESSAGE = `Sorry! This cannot be added because you are over your storage limit. 9 | Please delete some images or themes to make space.`; 10 | 11 | export const STORAGE_ERROR_MESSAGE_DURATION = 5000; 12 | 13 | export const localStorageSpace = () => { 14 | let size = 0; 15 | for (let [key, value] of Object.entries(localStorage)) { 16 | size += key.length + value.length; 17 | } 18 | 19 | const sizeMB = size / 1000000; 20 | return parseFloat(sizeMB.toFixed(3)); 21 | }; 22 | 23 | export const StorageSpaceInformationComponent = props => { 24 | React.useEffect(() => { 25 | let timer; 26 | if (props.storageErrorMessage.length) { 27 | timer = setTimeout( 28 | () => props.setStorageErrorMessage(""), 29 | STORAGE_ERROR_MESSAGE_DURATION 30 | ); 31 | } 32 | 33 | return function cleanup() { 34 | clearTimeout(timer); 35 | }; 36 | }, [props.storageErrorMessage]); 37 | 38 | return ( 39 |
40 |
41 | 42 | {props.usedStorage}MB out of 5.243MB 43 |
44 | {props.usedStorage > 4.5 && !props.storageErrorMessage && ( 45 |
46 | 47 | You have almost reached your storage limit!
You may need to 48 | delete some images or themes to make space. 49 |
50 |
51 | )} 52 | {props.storageErrorMessage && ( 53 | 54 |
55 | {props.storageErrorMessage} 56 |
57 |
58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export const mapStateToProps = state => { 64 | const { ui } = state; 65 | return { 66 | usedStorage: ui.usedStorage, 67 | storageErrorMessage: ui.storageErrorMessage 68 | }; 69 | }; 70 | 71 | const StorageSpaceInformation = connect( 72 | mapStateToProps, 73 | { setStorageErrorMessage: actions.ui.setStorageErrorMessage } 74 | )(StorageSpaceInformationComponent); 75 | 76 | export default StorageSpaceInformation; 77 | -------------------------------------------------------------------------------- /src/web/lib/components/StorageSpaceInformation/index.scss: -------------------------------------------------------------------------------- 1 | .storage-space-information { 2 | font-size: 12px; 3 | display: flex; 4 | justify-content: center; 5 | flex-direction: column; 6 | align-items: center; 7 | line-height: 1.5; 8 | } 9 | 10 | .storage-space-information-content { 11 | display: flex; 12 | justify-content: space-between; 13 | padding: 3px; 14 | 15 | span { 16 | padding: 0 5px; 17 | } 18 | } 19 | 20 | .storage-space-information-warning { 21 | text-align: center; 22 | line-height: 1.2; 23 | margin-top: 2px; 24 | } 25 | 26 | .storage-space-information-error { 27 | font-size: 14px; 28 | text-align: center; 29 | line-height: 1.1; 30 | max-width: 440px; 31 | } 32 | -------------------------------------------------------------------------------- /src/web/lib/components/TermsPrivacyModal/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Modal from "../Modal"; 4 | 5 | export const TermsPrivacyModal = ({ 6 | setDisplayLegalModal, 7 | displayLegalModal 8 | }) => ( 9 | 10 |

Terms & Privacy

11 |

12 | Use of the Firefox Color website is subject to Mozilla’s{" "} 13 | 14 | Websites Privacy Notice 15 | {" "} 16 | and{" "} 17 | 18 | Websites Terms of Use 19 | 20 | . 21 |

22 |
23 | ); 24 | 25 | export default TermsPrivacyModal; 26 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeBackgroundPicker/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ThemePatternPicker from "../ThemePatternPicker"; 3 | import ThemeCustomBackgroundPicker from "../ThemeCustomBackgroundPicker"; 4 | import StorageSpaceInformation from "../StorageSpaceInformation"; 5 | 6 | import "./index.scss"; 7 | 8 | export const ThemeBackgroundPicker = props => { 9 | const { theme, setBackground, themeHasCustomBackgrounds } = props; 10 | return ( 11 |
12 |
13 | {!themeHasCustomBackgrounds && ( 14 | 15 | )} 16 | 17 |
18 | 19 |
20 | ); 21 | }; 22 | 23 | export default ThemeBackgroundPicker; 24 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeBackgroundPicker/index.scss: -------------------------------------------------------------------------------- 1 | .theme-background-picker { 2 | display: flex; 3 | height: 100%; 4 | width: 100%; 5 | flex-direction: column; 6 | } 7 | 8 | .theme-background-picker-form { 9 | display: flex; 10 | height: 320px; 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeBuilder/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .theme-builder { 4 | -moz-user-select: none; 5 | border-top: 1px solid var(--grey-90-a20); 6 | user-select: none; 7 | width: 100%; 8 | 9 | .theme-builder__tabs-wrapper { 10 | background: var(--grey-20); 11 | border-bottom: 1px solid var(--grey-90-a10); 12 | display: flex; 13 | height: 48px; 14 | width: 100%; 15 | } 16 | 17 | .theme-builder__tabs { 18 | display: grid; 19 | grid-template-columns: repeat(auto-fill, minmax(20%, 1fr)); 20 | justify-content: center; 21 | width: 100%; 22 | } 23 | 24 | .theme-builder__tabs button { 25 | align-items: center; 26 | display: flex; 27 | height: 48px; 28 | justify-content: center; 29 | margin: 0; 30 | padding: 0; 31 | border: 0; 32 | position: relative; 33 | text-align: center; 34 | background: transparent; 35 | font-size: 16px; 36 | 37 | &::after { 38 | background: var(--grey-90-a10); 39 | content: ""; 40 | height: 2px; 41 | position: absolute; 42 | top: 0; 43 | transform-origin: center top; 44 | transform: scaleX(0); 45 | transition: transform 150ms ease-out; 46 | width: 100%; 47 | } 48 | 49 | &:hover:not(.theme-builder__selected), 50 | &:focus:not(.theme-builder__selected) { 51 | background: var(--grey-90-a05); 52 | 53 | &::after { 54 | transform: scaleX(1); 55 | } 56 | } 57 | 58 | &:active:not(.theme-builder__selected) { 59 | background: var(--grey-90-a10); 60 | } 61 | } 62 | 63 | button.theme-builder__selected { 64 | background: var(--white-100); 65 | border-left: 1px solid var(--grey-30); 66 | border-right: 1px solid var(--grey-30); 67 | color: var(--blue-60); 68 | 69 | &::after { 70 | background: var(--blue-40); 71 | transform: scaleX(1); 72 | } 73 | } 74 | 75 | button:first-child.theme-builder__selected { 76 | border-left: 0; 77 | } 78 | 79 | button:last-child.theme-builder__selected { 80 | border-right: 0; 81 | } 82 | 83 | .theme-builder__content { 84 | background: var(--white-100); 85 | margin: 0 auto; 86 | height: 425px; 87 | position: relative; 88 | display: flex; 89 | align-items: center; 90 | justify-content: center; 91 | outline: 0; 92 | flex-direction: column; 93 | } 94 | } 95 | 96 | @media (max-width: 940px) { 97 | .theme-builder__tabs { 98 | button { 99 | font-size: 14px; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeColorsEditor/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .theme-colors-editor { 4 | display: flex; 5 | justify-content: space-between; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .theme-colors-editor-main { 11 | width: 100%; 12 | } 13 | 14 | .theme-colors-editor__list { 15 | display: grid; 16 | grid-template-rows: repeat(20, 44px); 17 | grid-gap: $grid; 18 | grid-template-columns: 1fr 1fr; 19 | margin: 0 0 10px; 20 | padding: $grid * 4; 21 | position: relative; 22 | flex: 1 1 640px; 23 | height: 350px; 24 | overflow: auto; 25 | 26 | .theme-unit { 27 | align-items: center; 28 | display: flex; 29 | padding: 0 16px; 30 | cursor: pointer; 31 | transition: background 150ms; 32 | 33 | &:hover { 34 | background: var(--grey-90-a05); 35 | } 36 | 37 | &.selected { 38 | .theme-unit__label { 39 | color: var(--blue-50); 40 | } 41 | 42 | .theme-unit__picker { 43 | display: block; 44 | } 45 | 46 | .theme-unit__swatch { 47 | box-shadow: 0 0 0 2px var(--blue-50); 48 | } 49 | } 50 | } 51 | 52 | .theme-unit__swatch { 53 | border-radius: 2px; 54 | border: 1px solid var(--grey-90-a30); 55 | height: 20px; 56 | width: 20px; 57 | margin-right: $grid * 4; 58 | } 59 | } 60 | 61 | .theme-colors-editor__picker { 62 | flex: 0 0 270px; 63 | border-left: 1px solid var(--grey-30); 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: center; 67 | align-items: flex-start; 68 | 69 | .theme-colors-editor__prompt { 70 | align-items: center; 71 | background: var(--grey-10); 72 | display: flex; 73 | flex: 1; 74 | height: 100%; 75 | justify-content: center; 76 | padding: $grid * 3; 77 | } 78 | 79 | .theme-colors-editor__prompt-description { 80 | flex: 0 0 170px; 81 | } 82 | 83 | .theme-colors-editor__prompt-arrow { 84 | width: 24px; 85 | height: 24px; 86 | background: url("../../../../images/arrowhead-left.svg"); 87 | margin-right: $grid * 4; 88 | } 89 | } 90 | 91 | .theme-colors-editor__options { 92 | display: flex; 93 | flex-direction: column; 94 | } 95 | 96 | .theme-colors-editor__disabled { 97 | filter: grayscale(100%); 98 | } 99 | 100 | .theme-colors-editor__disabled:hover { 101 | filter: grayscale(70%); 102 | } 103 | 104 | .sketch-picker { 105 | box-sizing: border-box !important; 106 | box-shadow: none !important; 107 | border-radius: 0 !important; 108 | } 109 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/close.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/icon_align_center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/icon_align_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/icon_drag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | Drag 9 | Created with Sketch. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Created by rupa c 21 | from the Noun Project 22 | 23 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeCustomBackgroundPicker/icon_loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeLogger/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./index.scss"; 4 | 5 | const ThemeLogger = ({ theme }) => ( 6 |
7 |
{JSON.stringify(theme, null, 2)}
8 |
9 | ); 10 | 11 | export default ThemeLogger; 12 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeLogger/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .theme-logger { 4 | height: 100%; 5 | overflow: auto; 6 | background: #000; 7 | width: 100%; 8 | 9 | // NOTE, Kind of a bummer to use a prefix here, 10 | // But the unprefixed user-select selector does not 11 | // seem to work in Firefox 12 | // Filed https://bugzilla.mozilla.org/show_bug.cgi?id=1492739 13 | pre { 14 | color: lime; 15 | font-family: monospace; 16 | font-size: 14px; 17 | font-weight: bold; 18 | line-height: 20px; 19 | padding: 10px; 20 | user-select: text; 21 | -moz-user-select: text; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemePatternPicker/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import "./index.scss"; 4 | 5 | import { colorToCSS } from "../../../../lib/themes"; 6 | import { bgImages } from "../../../../lib/assets"; 7 | 8 | const Pattern = ({ src, backgroundId, active, setBackground, frame }) => ( 9 |
10 | { 15 | if (e.target.checked) { 16 | setBackground({ url: src }); 17 | } 18 | }} 19 | type="radio" 20 | /> 21 |
22 |
31 |
32 | ); 33 | 34 | class ThemeBackgroundPicker extends React.Component { 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | selected: false 39 | }; 40 | } 41 | 42 | handleClick = e => { 43 | if (e.target.classList.contains("theme-background-picker__backgrounds")) { 44 | return; 45 | } 46 | this.setState({ selected: !this.state.selected }); 47 | }; 48 | 49 | componentDidMount() { 50 | document.addEventListener("keydown", this.handleKeyPress); 51 | } 52 | 53 | componentWillUnmount() { 54 | document.removeEventListener("keydown", this.handleKeyPress); 55 | } 56 | 57 | render() { 58 | const { theme, setBackground } = this.props; 59 | const frame = colorToCSS(theme.colors.frame); 60 | return ( 61 |
62 |

Pick a pattern for your theme...

63 |
64 | {bgImages.keys().map((src, backgroundId) => ( 65 | 75 | ))} 76 |
77 |
78 | ); 79 | } 80 | } 81 | 82 | export default ThemeBackgroundPicker; 83 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemePatternPicker/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .theme-pattern-picker { 4 | display: flex; 5 | flex-direction: column; 6 | flex: 1; 7 | padding: 12px 0; 8 | 9 | p { 10 | text-align: center; 11 | margin-top: 10px; 12 | } 13 | 14 | /* https://stackoverflow.com/a/19758620 */ 15 | input[type="radio"] { 16 | position: absolute; 17 | width: 1px; 18 | height: 1px; 19 | padding: 0; 20 | margin: -1px; 21 | overflow: hidden; 22 | clip: rect(0, 0, 0, 0); 23 | border: 0; 24 | } 25 | 26 | .theme-pattern-picker__inner { 27 | display: grid; 28 | grid-template-columns: 1fr 1fr; 29 | grid-auto-rows: 64px; 30 | width: 100%; 31 | grid-gap: 24px; 32 | overflow: auto; 33 | padding: 12px 24px; 34 | margin-bottom: 12px; 35 | } 36 | 37 | input[type="radio"]:checked + div { 38 | box-shadow: 0 0 0 1px var(--white-100), 0 0 0 3px var(--blue-40); 39 | } 40 | 41 | input[type="radio"]:focus + div { 42 | box-shadow: 0 0 0 1px var(--white-100), 0 0 0 3px var(--grey-30); 43 | } 44 | 45 | .theme-pattern-picker__pattern { 46 | border-radius: 3px; 47 | overflow: hidden; 48 | transition: box-shadow 150ms; 49 | width: 100%; 50 | height: 100%; 51 | 52 | &:hover { 53 | box-shadow: 0 0 0 1px var(--white-100), 0 0 0 3px var(--grey-30); 54 | } 55 | } 56 | 57 | .theme-pattern-picker__color { 58 | display: block; 59 | width: 100%; 60 | height: 100%; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeSaveButton/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import { connect } from "react-redux"; 4 | 5 | export const ThemeSaveButtonComponent = ({ 6 | children, 7 | name, 8 | theme, 9 | storage, 10 | userHasEdited, 11 | modifiedSinceSave, 12 | setThemeBuilderPanel, 13 | dispatch 14 | }) => { 15 | const { themeStorage } = storage; 16 | const saveTheme = () => { 17 | if (!modifiedSinceSave) { 18 | return; 19 | } 20 | themeStorage.put(themeStorage.generateKey(), theme, dispatch); 21 | setThemeBuilderPanel(4); 22 | }; 23 | return ( 24 | 34 | ); 35 | }; 36 | 37 | const ThemeSaveButton = connect( 38 | null, 39 | dispatch => ({ dispatch }) 40 | )(ThemeSaveButtonComponent); 41 | 42 | export default ThemeSaveButton; 43 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeUrl/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | import onClickOutside from "react-onclickoutside"; 4 | 5 | import { themesEqual } from "../../../../lib/themes"; 6 | 7 | import "./index.scss"; 8 | 9 | class ThemeUrl extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | themeUrl: "", 14 | copied: false 15 | }; 16 | this.handleCopied = () => { 17 | this.setState({ copied: true }); 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | this.updateThemeUrl(this.props.theme); 23 | this.props.clipboard.on("success", this.handleCopied); 24 | } 25 | 26 | componentWillUnmount() { 27 | this.props.clipboard.off("success", this.handleCopied); 28 | } 29 | 30 | componentDidUpdate(prev) { 31 | const { theme } = this.props; 32 | if (!themesEqual(prev.theme, theme)) { 33 | this.updateThemeUrl(theme); 34 | } 35 | } 36 | 37 | handleClickOutside(evt) { 38 | if (evt.target.classList.contains("Share")) { 39 | return; 40 | } 41 | this.props.setDisplayShareModal({ display: false }); 42 | } 43 | 44 | handleCopied() { 45 | this.setState({ copied: true }); 46 | } 47 | 48 | updateThemeUrl(theme) { 49 | if (!theme) { 50 | return; 51 | } 52 | 53 | const { urlEncodeTheme } = this.props; 54 | urlEncodeTheme({ theme }).then(themeUrl => { 55 | this.setState({ themeUrl, copied: false }); 56 | }); 57 | } 58 | 59 | render() { 60 | const { copied, themeUrl } = this.state; 61 | const { themeHasCustomBackgrounds, hasExtension } = this.props; 62 | 63 | if (themeHasCustomBackgrounds) { 64 | return ( 65 |
70 |

71 | This theme cannot be shared via URL because it has a custom 72 | background image. 73 |

74 |
75 | ); 76 | } 77 | 78 | return ( 79 |
e.preventDefault()} 82 | > 83 | 86 | 87 | 94 |
95 | ); 96 | } 97 | } 98 | 99 | export default onClickOutside(ThemeUrl); 100 | -------------------------------------------------------------------------------- /src/web/lib/components/ThemeUrl/index.scss: -------------------------------------------------------------------------------- 1 | @import "../../common.scss"; 2 | 3 | .theme-url { 4 | background: var(--white-100); 5 | box-shadow: 0 0 0 1px var(--grey-90-a10), 0 10px 20px var(--grey-90-a20); 6 | display: flex; 7 | flex-wrap: wrap; 8 | flex: 1; 9 | padding: 10px; 10 | position: absolute; 11 | top: 60px; 12 | right: 8px; 13 | width: 280px; 14 | 15 | &.disabled { 16 | display: none; 17 | } 18 | 19 | &.extension { 20 | right: 70px; 21 | } 22 | 23 | &::after { 24 | content: ""; 25 | width: 0; 26 | height: 0; 27 | border-left: 8px solid transparent; 28 | border-right: 8px solid transparent; 29 | border-bottom: 8px solid white; 30 | position: absolute; 31 | top: -8px; 32 | right: 32px; 33 | } 34 | 35 | &::before { 36 | content: ""; 37 | width: 0; 38 | height: 0; 39 | border-left: 10px solid transparent; 40 | border-right: 10px solid transparent; 41 | border-bottom: 10px solid var(--grey-90-a10); 42 | position: absolute; 43 | top: -9px; 44 | right: 30px; 45 | z-index: -10; 46 | } 47 | 48 | h2 { 49 | margin-top: 0; 50 | cursor: text; 51 | } 52 | 53 | p { 54 | cursor: text; 55 | font-size: 14px; 56 | line-height: 1.5rem; 57 | } 58 | 59 | label { 60 | flex: 1 0 100%; 61 | } 62 | 63 | input[type="text"] { 64 | border-radius: 3px 0 0 3px; 65 | border: 1px solid var(--grey-30); 66 | font-size: 12px; 67 | height: $grid * 8; 68 | flex: 1; 69 | padding: 0 10px; 70 | } 71 | 72 | input[type="submit"] { 73 | @include buttonDefault; 74 | 75 | border-radius: 0 3px 3px 0; 76 | border: 0; 77 | flex: 0 0 72px; 78 | font-size: 12px; 79 | height: 32px; 80 | cursor: pointer; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/web/lib/export.js: -------------------------------------------------------------------------------- 1 | import JSZip from "jszip"; 2 | 3 | import { selectors } from "../../lib/store"; 4 | import { convertToBrowserTheme } from "../../lib/themes"; 5 | import { makeLog } from "../../lib/utils"; 6 | 7 | const log = makeLog("export"); 8 | 9 | export default function performThemeExport({ 10 | name = "Default Name", 11 | // TODO: Need other manifest attributes? 12 | store, 13 | bgImages 14 | }) { 15 | log("performThemeExport"); 16 | reset(); 17 | 18 | const zip = new JSZip(); 19 | const state = store.getState(); 20 | const themeData = selectors.theme(state); 21 | const customBackgrounds = selectors.themeCustomImages(state); 22 | const theme = convertToBrowserTheme(themeData, bgImages, customBackgrounds); 23 | 24 | if (theme.images) { 25 | const { images } = theme; 26 | const { additional_backgrounds } = images; 27 | if (images.theme_frame) { 28 | images.theme_frame = addImage(zip, images.theme_frame); 29 | } 30 | if (additional_backgrounds) { 31 | for (let idx = 0; idx < additional_backgrounds.length; idx++) { 32 | additional_backgrounds[idx] = addImage( 33 | zip, 34 | additional_backgrounds[idx] 35 | ); 36 | } 37 | } 38 | } 39 | 40 | const manifest = { 41 | manifest_version: 2, 42 | version: "1.0", 43 | name, 44 | theme 45 | }; 46 | log("manifest", manifest); 47 | zip.file("manifest.json", JSON.stringify(manifest, null, " ")); 48 | 49 | return Promise.all(pendingImages) 50 | .then(() => zip.generateAsync({ type: "base64" })) 51 | .then(data => "data:application/x-xpinstall;base64," + data); 52 | } 53 | 54 | let pendingImages = []; 55 | let currId = 0; 56 | const genId = () => currId++; 57 | 58 | function reset() { 59 | pendingImages = []; 60 | currId = 0; 61 | } 62 | 63 | const extensions = { 64 | "image/png": ".png", 65 | "image/jpeg": ".jpg", 66 | "image/bmp": ".bmp" 67 | }; 68 | 69 | function addImage(zip, data) { 70 | if (data.startsWith("data:")) { 71 | // Convert data: URL into binary file entry in zip 72 | const [meta, b64data] = data.split(",", 2); 73 | const [type] = meta.substr(5).split(/;/, 1); 74 | const filename = `images/${genId()}${extensions[type]}`; 75 | zip.file(filename, base64ToUint8array(b64data)); 76 | return filename; 77 | } 78 | 79 | if (data.startsWith("images/")) { 80 | // Convert file path into pending image fetch. 81 | const filename = data; 82 | pendingImages.push( 83 | fetch(filename) 84 | .then(response => response.blob()) 85 | .then(data => zip.file(filename, data)) 86 | ); 87 | return filename; 88 | } 89 | 90 | // TODO: Throw error? The previous conditions should be all known images 91 | return data; 92 | } 93 | 94 | function base64ToUint8array(s) { 95 | var byteChars = atob(s); 96 | var l = byteChars.length; 97 | var byteNumbers = new Array(l); 98 | for (var i = 0; i < l; i++) { 99 | byteNumbers[i] = byteChars.charCodeAt(i); 100 | } 101 | return new Uint8Array(byteNumbers); 102 | } 103 | -------------------------------------------------------------------------------- /src/web/robots.txt: -------------------------------------------------------------------------------- 1 | # Allow all robots complete access (no reason to exclude any, really) 2 | User-agent: * 3 | Disallow: -------------------------------------------------------------------------------- /src/web/testing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Unsigned Addons 7 | 8 | 9 | This is a test page. On production at , the "Install" link at the home page directly refers to 10 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /webpack.extension.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | 3 | const path = require("path"); 4 | const merge = require("webpack-merge"); 5 | 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | const GenerateAssetWebpackPlugin = require("generate-asset-webpack-plugin"); 8 | 9 | const packageMeta = require("./package.json"); 10 | const { 11 | siteUrl, 12 | siteId, 13 | nodeEnv, 14 | webpackConfig 15 | } = require("./webpack.common.js"); 16 | 17 | module.exports = merge(webpackConfig, { 18 | entry: { 19 | background: "./src/extension/background", 20 | contentScript: "./src/extension/contentScript" 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, "build/extension"), 24 | filename: "[name].js" 25 | }, 26 | optimization: { 27 | minimize: false 28 | }, 29 | node: { 30 | global: false 31 | }, 32 | plugins: [ 33 | new GenerateAssetWebpackPlugin({ 34 | filename: "manifest.json", 35 | fn: buildManifest 36 | }), 37 | new CopyWebpackPlugin({ 38 | patterns: [ 39 | { from: "LICENSE" }, 40 | { from: "src/images/icon.svg", to: "images/" }, 41 | { from: "src/images/logo.svg", to: "images/" } 42 | ] 43 | }) 44 | ] 45 | }); 46 | 47 | function buildManifest(compilation, cb) { 48 | const { 49 | name, 50 | version, 51 | description, 52 | author, 53 | homepage, 54 | extensionManifest 55 | } = packageMeta; 56 | 57 | const manifest = Object.assign( 58 | {}, 59 | // HACK: Quick & dirty clone of extensionManifest to break references, so 60 | // that later modifications don't change the cached data from package.json 61 | JSON.parse(JSON.stringify(extensionManifest)), 62 | { 63 | manifest_version: 2, 64 | // HACK: Accept override in extensionManifest - npm disallows caps & 65 | // spaces, but we want them in an extension name 66 | name: extensionManifest.name || name, 67 | version, 68 | description, 69 | author, 70 | homepage_url: homepage 71 | } 72 | ); 73 | 74 | let idSuffix = []; 75 | if (siteId) { 76 | idSuffix.push(siteId); 77 | } 78 | if (nodeEnv === "development") { 79 | idSuffix.push("dev"); 80 | } 81 | if (idSuffix.length) { 82 | idSuffix = idSuffix.join("-"); 83 | manifest.applications.gecko.id = manifest.applications.gecko.id.replace( 84 | "@", 85 | `-${idSuffix}@` 86 | ); 87 | manifest.name = `${manifest.name} (${idSuffix})`; 88 | } 89 | 90 | // Configure content script to run on SITE_URL, omitting port if any 91 | manifest.content_scripts[0].matches = [ 92 | `${siteUrl.replace(/:(\d+)\/?$/, "/")}*` 93 | ]; 94 | 95 | return cb(null, JSON.stringify(manifest, null, " ")); 96 | } 97 | -------------------------------------------------------------------------------- /webpack.web.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: off */ 2 | const webpack = require("webpack"); 3 | 4 | const path = require("path"); 5 | const merge = require("webpack-merge"); 6 | const { exec } = require("child_process"); 7 | 8 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 9 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 10 | const GenerateAssetWebpackPlugin = require("generate-asset-webpack-plugin"); 11 | 12 | const packageMeta = require("./package.json"); 13 | const common = require("./webpack.common.js"); 14 | 15 | module.exports = merge(common.webpackConfig, { 16 | entry: { 17 | index: "./src/web/index" 18 | }, 19 | devServer: { 20 | hot: true, 21 | contentBase: path.join(__dirname, "build/web"), 22 | host: common.siteHost, 23 | port: common.sitePort 24 | }, 25 | output: { 26 | path: path.resolve(__dirname, "build/web"), 27 | chunkFilename: "[name].chunk.js", 28 | filename: "[name].js" 29 | }, 30 | plugins: [ 31 | new webpack.HotModuleReplacementPlugin(), 32 | new GenerateAssetWebpackPlugin({ 33 | filename: "__version__", 34 | fn: buildVersionJSON 35 | }), 36 | new HtmlWebpackPlugin({ 37 | template: "./src/web/index.html.ejs", 38 | filename: "index.html", 39 | chunks: ["index"], 40 | ogImage: `${common.siteUrl}images/color-fb.jpg`, 41 | twitterImage: `${common.siteUrl}images/color-twitter.jpg`, 42 | title: packageMeta.title, 43 | description: packageMeta.description, 44 | homepage: common.siteUrl 45 | }), 46 | new CopyWebpackPlugin({ 47 | patterns: [ 48 | { from: "./src/web/testing.html", to: "testing.html" }, 49 | { from: "./src/web/robots.txt", to: "robots.txt" }, 50 | { from: "./src/web/favicon.ico", to: "favicon.ico" }, 51 | { from: "./src/images", to: "images" }, 52 | // FIXME: Bundling this in webpack causes it to fail, just copy for now 53 | { from: "./node_modules/json-url/dist/browser", to: "vendor" } 54 | ] 55 | }) 56 | ] 57 | }); 58 | 59 | function buildVersionJSON(compilation, cb) { 60 | exec('git --no-pager log --format=format:"%H" -1', (err, stdout, stderr) => { 61 | cb( 62 | null, 63 | JSON.stringify( 64 | { 65 | commit: err ? "" : stdout, 66 | version: packageMeta.version, 67 | source: `https://github.com/${packageMeta.repository}` 68 | }, 69 | null, 70 | " " 71 | ) 72 | ); 73 | }); 74 | } 75 | --------------------------------------------------------------------------------