├── .editorconfig ├── .ember-cli ├── .github ├── dependabot.yml └── workflows │ ├── electron.yml │ ├── ember.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .template-lintrc.js ├── .tool-versions ├── .vscode └── settings.json ├── .watchmanconfig ├── CHANGELOG.md ├── README.md ├── RELEASE.md ├── add-osx-cert.sh ├── app ├── app.ts ├── authenticators │ └── cognito.js ├── components │ ├── about.hbs │ ├── about.ts │ ├── alpha-input.hbs │ ├── alpha-input.ts │ ├── animated-drag-sort-list.hbs │ ├── animated-drag-sort-list.ts │ ├── color-picker.hbs │ ├── color-picker.ts │ ├── color-row.hbs │ ├── color-row.ts │ ├── colors-list.hbs │ ├── colors-list.ts │ ├── contrast-checker.hbs │ ├── contrast-checker.ts │ ├── edit-selected-color.hbs │ ├── edit-selected-color.ts │ ├── forgot-password.hbs │ ├── forgot-password.ts │ ├── hex-input.hbs │ ├── hex-input.ts │ ├── kuler-palette-row.hbs │ ├── kuler-palette-row.ts │ ├── kuler.hbs │ ├── kuler.ts │ ├── loading-button.hbs │ ├── loading-button.ts │ ├── login.hbs │ ├── login.ts │ ├── options-menu.hbs │ ├── options-menu.ts │ ├── palette-row.hbs │ ├── palette-row.ts │ ├── palettes-list.hbs │ ├── palettes-list.ts │ ├── register-confirm.hbs │ ├── register-confirm.ts │ ├── register.hbs │ ├── register.ts │ ├── rgb-input.hbs │ ├── rgb-input.ts │ ├── settings-data.hbs │ ├── settings-data.ts │ ├── settings-menu.hbs │ ├── settings-menu.ts │ ├── settings-nav.hbs │ └── toggle-switch.gts ├── config │ └── environment.d.ts ├── controllers │ ├── application.ts │ ├── colors.ts │ ├── contrast.ts │ ├── kuler.ts │ ├── palettes.ts │ ├── settings.ts │ ├── settings │ │ ├── cloud │ │ │ └── profile.ts │ │ └── index.ts │ ├── welcome.ts │ └── welcome │ │ ├── auto-start.ts │ │ ├── dock-icon.ts │ │ └── index.ts ├── data-buckets │ ├── .gitkeep │ └── main.js ├── data-models │ ├── .gitkeep │ ├── color.ts │ └── palette.ts ├── data-sources │ ├── .gitkeep │ ├── backup.ts │ └── remote.js ├── data-strategies │ ├── .gitkeep │ ├── event-logging.js │ ├── remote-queryfail.js │ ├── remote-store-sync.js │ ├── remote-updatefail.js │ ├── store-backup-sync.ts │ ├── store-beforequery-remote-query.js │ └── store-beforeupdate-remote-update.js ├── deprecation-workflow.ts ├── helpers │ ├── capitalize.ts │ └── html-safe.ts ├── index.html ├── initializers │ └── main-bucket-initializer.js ├── router.ts ├── routes │ ├── -private │ │ └── application.ts │ ├── application.js │ ├── colors.ts │ ├── contrast.ts │ ├── index.ts │ ├── kuler.ts │ ├── palettes.ts │ └── settings │ │ ├── cloud │ │ ├── index.ts │ │ └── profile.ts │ │ ├── data.ts │ │ └── index.ts ├── services │ ├── cognito.js │ ├── color-utils.ts │ ├── data.ts │ ├── nearest-color.ts │ ├── session.ts │ └── undo-manager.ts ├── session-stores │ └── application.ts ├── storages │ └── settings.ts ├── styles │ ├── app.css │ ├── color-squares.css │ ├── drag-sort.css │ ├── fonts.css │ ├── icons.css │ ├── main.css │ ├── popover.css │ ├── tailwind.css │ ├── three-dots │ │ ├── _dot-typing.css │ │ ├── _variables.css │ │ └── three-dots.css │ └── variables.css ├── templates │ ├── application.hbs │ ├── colors.hbs │ ├── contrast.hbs │ ├── kuler.hbs │ ├── palettes.hbs │ ├── settings.hbs │ ├── settings │ │ ├── cloud.hbs │ │ ├── cloud │ │ │ ├── forgot-password.hbs │ │ │ ├── login.hbs │ │ │ ├── profile.hbs │ │ │ └── register │ │ │ │ ├── confirm.hbs │ │ │ │ └── index.hbs │ │ ├── data.hbs │ │ └── index.hbs │ ├── welcome.hbs │ └── welcome │ │ ├── auto-start.hbs │ │ ├── cloud-sync.hbs │ │ ├── dock-icon.hbs │ │ └── index.hbs ├── transitions.js └── utils │ ├── get-db-open-request.ts │ ├── remove-from-to.ts │ └── view-transitions.ts ├── config ├── content-security-policy.js ├── ember-cli-update.json ├── environment.js ├── optional-features.json └── targets.js ├── electron-app ├── .gitignore ├── example.sentry.properties ├── forge.config.js ├── package.json ├── resources │ ├── .gitkeep │ ├── dmg.icns │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── installBackground.png │ ├── installBackground@2x.png │ ├── menubar-icons │ │ ├── iconTemplate.png │ │ └── iconTemplate@2x.png │ └── png │ │ ├── 1024x1024.png │ │ ├── 128x128.png │ │ ├── 16x16.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 32x32.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 64x64.png │ │ └── 96x96.png ├── sentry-symbols.js ├── src │ ├── auto-update.js │ ├── browsers │ │ ├── index.js │ │ └── window.js │ ├── color-picker.js │ ├── dialogs.js │ ├── entitlements.plist │ ├── handle-file-urls.js │ ├── index.js │ ├── ipc-events.js │ ├── preload.js │ └── shortcuts.js └── tests │ └── index.js ├── ember-cli-build.js ├── eslint.config.mjs ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── assets │ ├── fonts │ │ └── inter-variable.otf │ └── sounds │ │ ├── marimba_chromatic.wav │ │ └── pluck_short.wav ├── robots.txt └── svgs │ ├── alert-circle.svg │ ├── appearance │ ├── dark-theme.svg │ ├── dynamic-theme.svg │ └── light-theme.svg │ ├── arrow-left.svg │ ├── check.svg │ ├── chevron-left.svg │ ├── clear-history.svg │ ├── close.svg │ ├── cloud.svg │ ├── color-harmonies.svg │ ├── contrast.svg │ ├── drop.svg │ ├── duplicate.svg │ ├── edit-color.svg │ ├── edit.svg │ ├── filled-heart.svg │ ├── lock.svg │ ├── menu.svg │ ├── more-horizontal.svg │ ├── outline-heart.svg │ ├── palettes.svg │ ├── plus-circle.svg │ ├── plus.svg │ ├── rename.svg │ ├── save.svg │ ├── settings.svg │ ├── share.svg │ ├── show-dock-icon.svg │ ├── slash-heart.svg │ ├── swach.svg │ ├── trash.svg │ ├── unlock.svg │ └── x.svg ├── tailwind.config.js ├── testem-electron.js ├── testem.js ├── tests ├── acceptance │ ├── color-picker-test.js │ ├── colors-test.js │ ├── contrast-test.js │ ├── from-scratch-test.js │ ├── index-test.js │ ├── kuler-test.js │ ├── palettes-test.js │ ├── settings-test.js │ ├── settings │ │ ├── cloud-test.ts │ │ └── data-test.ts │ └── welcome-test.ts ├── helpers.ts ├── helpers │ ├── flash-message.js │ └── index.ts ├── index.html ├── integration │ └── components │ │ └── contrast-checker-test.js ├── orbit │ ├── fixtures │ │ ├── colors.js │ │ ├── colors │ │ │ ├── color-history.js │ │ │ ├── first-palette.js │ │ │ ├── locked-palette.js │ │ │ └── second-palette.js │ │ └── palettes.js │ └── seed.js ├── test-helper.ts └── unit │ ├── services │ └── data-test.ts │ └── utils │ └── remove-from-to-test.ts ├── tsconfig.json └── types ├── electron └── index.d.ts ├── ember-animated ├── motions │ ├── move.d.ts │ └── opacity.d.ts └── transitions │ └── fade │ └── index.d.ts ├── ember-cognito ├── services │ └── cognito.d.ts └── test-support │ └── index.d.ts ├── ember-drag-sort ├── components │ └── drag-sort-list.d.ts └── services │ └── drag-sort.d.ts ├── ember-local-storage ├── index.d.ts ├── local │ └── object.d.ts └── test-support │ └── reset-storage.d.ts ├── ember-sinon-qunit └── index.d.ts ├── global.d.ts ├── indexeddb-export-import └── index.d.ts ├── nearest-color └── index.d.ts ├── swach └── index.d.ts ├── throttle-debounce └── index.d.ts └── wcag-contrast └── index.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 5 | */ 6 | "isTypeScriptProject": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | labels: 10 | - dependencies 11 | ignore: 12 | - dependency-name: ember-cli 13 | versions: 14 | - ">= 0" 15 | -------------------------------------------------------------------------------- /.github/workflows/electron.yml: -------------------------------------------------------------------------------- 1 | name: Electron 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | jobs: 10 | test: 11 | name: Tests 12 | runs-on: ubuntu-22.04 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: 'pnpm' 21 | - run: pnpm install 22 | - name: Get xvfb 23 | run: sudo apt-get install xvfb 24 | - name: Electron Test 25 | run: xvfb-run --auto-servernum pnpm test:electron -------------------------------------------------------------------------------- /.github/workflows/ember.yml: -------------------------------------------------------------------------------- 1 | name: Ember 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | jobs: 10 | test: 11 | name: Tests 12 | runs-on: ubuntu-22.04 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: 'pnpm' 21 | - run: pnpm install 22 | - name: Ember Test 23 | run: pnpm test:ember -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: {} 8 | 9 | jobs: 10 | lint: 11 | name: JS and HBS 12 | runs-on: ubuntu-22.04 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | cache: 'pnpm' 21 | - run: pnpm install 22 | - name: Lint JS 23 | run: pnpm lint:js 24 | - name: Lint HBS 25 | run: pnpm lint:hbs 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /declarations/ 6 | 7 | # dependencies 8 | /node_modules/ 9 | 10 | # misc 11 | /.env* 12 | /.pnp* 13 | /.sass-cache 14 | /.eslintcache 15 | /connect.lock 16 | /coverage/ 17 | /libpeerconnection.log 18 | /npm-debug.log* 19 | /testem.log 20 | /yarn-error.log 21 | 22 | # broccoli-debug 23 | /DEBUG/ 24 | 25 | electron-out/ 26 | .idea/ 27 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | shamefully-hoist=true 3 | side-effects-cache=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | /pnpm-lock.yaml 12 | ember-cli-update.json 13 | *.html 14 | 15 | # ember-try 16 | /.node_modules.ember-try/ 17 | 18 | # ember-electron 19 | /electron-app/node_modules/ 20 | /electron-app/out/ 21 | /electron-app/ember-dist/ 22 | /electron-app/ember-test/ 23 | 24 | # Sentry 25 | /electron-app/sentry-symbols.js 26 | 27 | # Types 28 | /types/ 29 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const testing = [ 4 | '^ember-cli-htmlbars($|\\/)', 5 | '^qunit', 6 | '^ember-qunit', 7 | '^@ember/test-helpers', 8 | '^ember-exam', 9 | '^ember-cli-mirage', 10 | '^sinon', 11 | '^ember-sinon-qunit', 12 | '^(@[^\\/]+\\/)?[^\\/]+\\/test-support($|\\/)', 13 | ].join('|'); 14 | 15 | const emberCore = [ 16 | '^ember$', 17 | '^@ember\\/', 18 | '^ember-data($|\\/)', 19 | '^@ember-data\\/', 20 | '^@glimmer\\/', 21 | '^require$', 22 | ].join('|'); 23 | 24 | const emberAddons = ['^@?ember-', '^@[^\\/]+\\/ember($|\\/|-)'].join('|'); 25 | 26 | const swachInternals = ['^swach/(.*)$', '^[./]'].join('|'); 27 | 28 | const importOrder = [ 29 | testing, 30 | emberCore, 31 | emberAddons, 32 | '', 33 | swachInternals, 34 | ]; 35 | const importOrderParserPlugins = ['typescript', 'decorators-legacy']; 36 | const importOrderSeparation = true; 37 | const importOrderSortSpecifiers = true; 38 | 39 | module.exports = { 40 | plugins: [ 41 | 'prettier-plugin-ember-template-tag', 42 | '@trivago/prettier-plugin-sort-imports', 43 | ], 44 | importOrder, 45 | importOrderParserPlugins, 46 | importOrderSeparation, 47 | importOrderSortSpecifiers, 48 | overrides: [ 49 | { 50 | files: '*.{js,gjs,ts,gts,mjs,mts,cjs,cts}', 51 | options: { singleQuote: true, templateSingleQuote: false }, 52 | }, 53 | { files: '*.{yaml,yml}', options: { singleQuote: true } }, 54 | ], 55 | }; 56 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # unconventional files 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['stylelint-config-standard'], 5 | rules: { 6 | 'at-rule-no-deprecated': [true, { ignoreAtRules: ['/^view/', 'apply'] }], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: ['recommended'], 5 | rules: { 6 | 'no-at-ember-render-modifiers': false, 7 | 'no-builtin-form-components': false, 8 | 'no-curly-component-invocation': { 9 | allow: ['svg-jar', '-with-dynamic-vars'], 10 | }, 11 | 'no-inline-styles': false, 12 | 'no-invalid-interactive': false, 13 | 'no-negated-condition': false, 14 | 'no-outlet-outside-routes': false, 15 | 'require-input-label': false, 16 | 'require-presentational-children': false, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.19.0 2 | pnpm 10.11.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "glimmer-ts", 4 | "glimmer-js" 5 | ] 6 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swach 2 | 3 | ![CI](https://github.com/shipshapecode/swach/workflows/CI/badge.svg) 4 | 5 | Swach is a modern color palette manager. 6 | 7 | 8 | Swach homepage showing the app running. 12 | 13 | 14 | **[Swach is built and maintained by Ship Shape. Contact us for web and native app development services.](https://shipshape.io/)** 15 | 16 | ## Prerequisites 17 | 18 | You will need the following things properly installed on your computer. 19 | 20 | - [Git](https://git-scm.com/) 21 | - [Node.js](https://nodejs.org/) 22 | - [pnpm](https://pnpm.io/) 23 | - [Ember CLI](https://ember-cli.com/) 24 | - [Google Chrome](https://google.com/chrome/) 25 | 26 | ## Installation 27 | 28 | - `git clone ` this repository 29 | - `cd swach` 30 | - `pnpm install` 31 | 32 | ## Running / Development 33 | 34 | ### Electron 35 | 36 | - `ember electron` 37 | 38 | ### Ember 39 | 40 | - `ember serve` 41 | - Visit your app at [http://localhost:4200](http://localhost:4200). 42 | - Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). 43 | 44 | ### Running Tests 45 | 46 | - `ember test` 47 | - `ember test --server` 48 | 49 | ### Linting 50 | 51 | - `pnpm lint` 52 | - `pnpm lint:fix` 53 | 54 | ### Building / Packaging 55 | 56 | - `ember electron:make` 57 | 58 | ## Releasing 59 | 60 | - Bump the version with: 61 | 62 | ```bash 63 | release-it 64 | ``` 65 | 66 | Choose the appropriate major, minor, patch, beta, etc version in the prompt. 67 | 68 | GitHub actions should then take that new tag and build and release automatically. 69 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | - breaking - Used when the PR is considered a breaking change. 21 | - enhancement - Used when the PR adds a new feature or enhancement. 22 | - bug - Used when the PR fixes a bug included in a previous release. 23 | - documentation - Used when the PR adds or updates documentation. 24 | - internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | - First, ensure that you have installed your projects dependencies: 32 | 33 | ``` 34 | pnpm install 35 | ``` 36 | 37 | - Second, ensure that you have obtained a 38 | [GitHub personal access token][generate-token] with the `repo` scope (no 39 | other permissions are needed). Make sure the token is available as the 40 | `GITHUB_AUTH` environment variable. 41 | 42 | For instance: 43 | 44 | ```bash 45 | export GITHUB_AUTH=abc123def456 46 | ``` 47 | 48 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 49 | 50 | - And last (but not least 😁) do your release. 51 | 52 | ``` 53 | npx release-it 54 | ``` 55 | 56 | [release-it](https://github.com/release-it/release-it/) manages the actual 57 | release process. It will prompt you to to choose the version number after which 58 | you will have the chance to hand tweak the changelog to be used (for the 59 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 60 | pushing the tag and commits, etc. 61 | -------------------------------------------------------------------------------- /add-osx-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | KEY_CHAIN=build.keychain 4 | CERTIFICATE_P12=certificate.p12 5 | 6 | # Recreate the certificate from the secure environment variable 7 | echo $CERTIFICATE_OSX_APPLICATION | base64 --decode > $CERTIFICATE_P12 8 | 9 | #create a keychain 10 | security create-keychain -p actions $KEY_CHAIN 11 | 12 | # Make the keychain the default so identities are found 13 | security default-keychain -s $KEY_CHAIN 14 | 15 | # Unlock the keychain 16 | security unlock-keychain -p actions $KEY_CHAIN 17 | 18 | security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign; 19 | 20 | security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN 21 | 22 | # remove certs 23 | rm -fr *.p12 -------------------------------------------------------------------------------- /app/app.ts: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | 3 | import { InitSentryForEmber } from '@sentry/ember'; 4 | import loadInitializers from 'ember-load-initializers'; 5 | import Resolver from 'ember-resolver'; 6 | 7 | import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros'; 8 | 9 | import config from 'swach/config/environment'; 10 | 11 | if (macroCondition(isDevelopingApp())) { 12 | importSync('./deprecation-workflow'); 13 | } 14 | 15 | InitSentryForEmber(); 16 | 17 | export default class App extends Application { 18 | modulePrefix = config.modulePrefix; 19 | podModulePrefix = config.podModulePrefix; 20 | Resolver = Resolver; 21 | } 22 | 23 | loadInitializers(App, config.modulePrefix); 24 | -------------------------------------------------------------------------------- /app/authenticators/cognito.js: -------------------------------------------------------------------------------- 1 | import { service } from '@ember/service'; 2 | 3 | import CognitoAuthenticator from 'ember-cognito/authenticators/cognito'; 4 | 5 | export default class CognitoAuthenticatorExtended extends CognitoAuthenticator { 6 | @service cognito; 7 | 8 | _makeAuthData(user, session, credentials) { 9 | return { 10 | poolId: user.pool.getUserPoolId(), 11 | clientId: user.pool.getClientId(), 12 | sessionCredentials: credentials, 13 | access_token: session.getIdToken().getJwtToken(), 14 | }; 15 | } 16 | 17 | async _resolveAuth(user) { 18 | const { cognito } = this; 19 | 20 | cognito._setUser(user); 21 | 22 | // Now pull out the (promisified) user 23 | const session = await cognito.user.getSession(); 24 | const credentials = await this.auth.currentCredentials(); 25 | 26 | cognito.startRefreshTask(session); 27 | 28 | return this._makeAuthData(user, session, credentials); 29 | } 30 | 31 | async _handleRefresh() { 32 | const { cognito } = this; 33 | const { auth, user } = cognito; 34 | // Get the session, which will refresh it if necessary 35 | const session = await user.getSession(); 36 | 37 | if (session.isValid()) { 38 | cognito.startRefreshTask(session); 39 | 40 | const awsUser = await auth.currentAuthenticatedUser(); 41 | const credentials = await this.auth.currentCredentials(); 42 | 43 | return this._makeAuthData(awsUser, session, credentials); 44 | } else { 45 | throw new Error('session is invalid'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/components/about.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | About 4 |
5 | 6 |

7 | Version: 8 | {{this.version}} 9 |

10 | 11 |

12 | Copyright © 13 | {{this.copyrightYear}} 14 | Ship Shape Consulting LLC. 15 |

16 | 17 |

18 | All rights reserved. 19 |

20 | 21 |

22 | 23 | https://swach.io/ 24 | 25 |

26 |
-------------------------------------------------------------------------------- /app/components/about.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Owner from '@ember/owner'; 3 | import Component from '@glimmer/component'; 4 | import { tracked } from '@glimmer/tracking'; 5 | 6 | import type { IpcRenderer } from 'electron'; 7 | 8 | interface AboutSignature { 9 | Element: HTMLDivElement; 10 | } 11 | 12 | export default class AboutComponent extends Component { 13 | declare ipcRenderer: IpcRenderer; 14 | 15 | copyrightYear = new Date().getFullYear(); 16 | @tracked version = 'Version not available'; 17 | 18 | constructor(owner: Owner, args: Record) { 19 | super(owner, args); 20 | 21 | if (typeof requireNode !== 'undefined') { 22 | const { ipcRenderer } = requireNode('electron'); 23 | 24 | this.ipcRenderer = ipcRenderer; 25 | 26 | void this.ipcRenderer.invoke('getAppVersion').then((version: string) => { 27 | this.version = version; 28 | }); 29 | } 30 | } 31 | 32 | @action 33 | visitWebsite(event: Event): void { 34 | event.preventDefault(); 35 | 36 | if (typeof requireNode !== 'undefined') { 37 | void this.ipcRenderer.invoke('open-external', 'https://swach.io/'); 38 | } 39 | } 40 | } 41 | 42 | declare module '@glint/environment-ember-loose/registry' { 43 | export default interface Registry { 44 | About: typeof AboutComponent; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/components/alpha-input.hbs: -------------------------------------------------------------------------------- 1 | 2 | A: 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/components/alpha-input.ts: -------------------------------------------------------------------------------- 1 | import { action, set } from '@ember/object'; 2 | import Component from '@glimmer/component'; 3 | 4 | import type { SelectedColorModel } from 'swach/components/rgb-input'; 5 | import { rgbaToHex } from 'swach/data-models/color'; 6 | 7 | interface AlphaInputSignature { 8 | Element: HTMLInputElement; 9 | Args: { 10 | selectedColor: SelectedColorModel; 11 | update: (value: string | number) => void; 12 | updateColor: () => void; 13 | value?: string; 14 | }; 15 | } 16 | 17 | export default class AlphaInputComponent extends Component { 18 | alphaRegex = /^[1]$|^[0]$|^(0\.[0-9]{1,2})$/; 19 | 20 | @action 21 | enterPress(event: KeyboardEvent): void { 22 | if (event.keyCode === 13) { 23 | (event.target).blur(); 24 | } 25 | } 26 | 27 | @action 28 | isComplete(buffer: Buffer, opts: { regex: string }): boolean { 29 | const value = buffer.join(''); 30 | 31 | return Boolean(value.length) && new RegExp(opts.regex).test(value); 32 | } 33 | 34 | /** 35 | * When the rgb input value passes the regex, set the value, and update the hex values and color. 36 | * @param {Event} event 37 | */ 38 | @action 39 | onComplete(event: InputEvent): void { 40 | const { selectedColor } = this.args; 41 | let value = parseFloat((event.target).value); 42 | 43 | if (value > 1) { 44 | value = 1; 45 | } 46 | 47 | set(selectedColor, 'a', value); 48 | set(selectedColor, `_a`, value); 49 | 50 | const { r, g, b, a } = selectedColor; 51 | const hex = rgbaToHex(r, g, b, a); 52 | 53 | set(selectedColor, '_hex', hex); 54 | set(selectedColor, 'hex', hex); 55 | this.args.updateColor(); 56 | } 57 | 58 | /** 59 | * Resets the alpha input value if you navigate away 60 | */ 61 | @action 62 | onIncomplete(): void { 63 | set(this.args.selectedColor, `_a`, this.args.selectedColor.a); 64 | } 65 | } 66 | 67 | declare module '@glint/environment-ember-loose/registry' { 68 | export default interface Registry { 69 | AlphaInput: typeof AlphaInputComponent; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/components/animated-drag-sort-list.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#animated-each @items duration=400 rules=this.rules as |item index|}} 3 | 19 | {{yield item index}} 20 | 21 | {{/animated-each}} 22 | -------------------------------------------------------------------------------- /app/components/animated-drag-sort-list.ts: -------------------------------------------------------------------------------- 1 | import { action, set } from '@ember/object'; 2 | 3 | import type Sprite from 'ember-animated/-private/sprite'; 4 | import { easeOut } from 'ember-animated/easings/cosine'; 5 | import move from 'ember-animated/motions/move'; 6 | import { fadeOut } from 'ember-animated/motions/opacity'; 7 | import DragSortList from 'ember-drag-sort/components/drag-sort-list'; 8 | 9 | export default class AnimatedDragSortList extends DragSortList { 10 | didDrag = false; 11 | 12 | dragEnter(event: Event): void { 13 | set(this, 'didDrag', true); 14 | super.dragEnter(event); 15 | } 16 | 17 | @action 18 | rules(): unknown { 19 | if (!this.didDrag) { 20 | // eslint-disable-next-line @typescript-eslint/unbound-method 21 | return this.transition; 22 | } 23 | 24 | set(this, 'didDrag', false); 25 | 26 | return null; 27 | } 28 | 29 | // eslint-disable-next-line require-yield 30 | *transition({ 31 | keptSprites, 32 | insertedSprites, 33 | removedSprites, 34 | }: { 35 | keptSprites: Array; 36 | insertedSprites: Array; 37 | removedSprites: Array; 38 | }): unknown { 39 | for (const sprite of insertedSprites) { 40 | if (sprite.finalBounds?.height) { 41 | sprite.startTranslatedBy(0, -sprite.finalBounds.height / 2); 42 | } 43 | 44 | move(sprite, { easing: easeOut }); 45 | } 46 | 47 | for (const sprite of keptSprites) { 48 | move(sprite, { easing: easeOut }); 49 | } 50 | 51 | for (const sprite of removedSprites) { 52 | fadeOut(sprite, { easing: easeOut }); 53 | } 54 | } 55 | } 56 | 57 | declare module '@glint/environment-ember-loose/registry' { 58 | export default interface Registry { 59 | AnimatedDragSortList: typeof AnimatedDragSortList; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/components/color-row.hbs: -------------------------------------------------------------------------------- 1 |
6 |
7 |
11 |
14 |
15 | 16 |
17 |
18 |

19 | {{@color.name}} 20 |

21 |

25 | {{@color.hex}} 26 |

27 |
28 | 29 | {{#if this.showActions}} 30 | 35 | <:trigger> 36 | {{svg-jar "more-horizontal" class="icon" height="15" width="15"}} 37 | 38 | <:content> 39 | 48 | 62 | {{#if @deleteColor}} 63 | 73 | {{/if}} 74 | 75 | 76 | {{/if}} 77 |
78 |
-------------------------------------------------------------------------------- /app/components/color-row.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | import { isEmpty } from '@ember/utils'; 5 | import Component from '@glimmer/component'; 6 | 7 | import type ColorModel from 'swach/data-models/color'; 8 | import type PaletteModel from 'swach/data-models/palette'; 9 | import type ColorUtils from 'swach/services/color-utils'; 10 | 11 | interface ColorRowSignature { 12 | Element: HTMLDivElement; 13 | Args: { 14 | color: ColorModel; 15 | deleteColor?: (color: ColorModel) => void; 16 | palette: PaletteModel; 17 | showActions: boolean; 18 | toggleColorPickerIsShown: (color?: ColorModel) => void; 19 | }; 20 | } 21 | 22 | export default class ColorRowComponent extends Component { 23 | @service declare colorUtils: ColorUtils; 24 | @service declare router: Router; 25 | 26 | get showActions() { 27 | if (isEmpty(this.args.showActions)) { 28 | return true; 29 | } 30 | 31 | return this.args.showActions; 32 | } 33 | 34 | @action 35 | deleteColor(color: ColorModel): void { 36 | if (!this.args.palette.isLocked) { 37 | this.args.deleteColor?.(color); 38 | } 39 | } 40 | 41 | @action 42 | transitionToKuler(event: Event): void { 43 | event.stopPropagation(); 44 | this.router.transitionTo('kuler', { 45 | queryParams: { colorId: this.args.color.id }, 46 | }); 47 | } 48 | } 49 | 50 | declare module '@glint/environment-ember-loose/registry' { 51 | export default interface Registry { 52 | ColorRow: typeof ColorRowComponent; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/components/colors-list.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#animated-each 3 | this.sortedColors duration=400 use=this.transition 4 | as |color| 5 | }} 6 | {{! TODO: remove this disconnected check when caching is fixed in ember-orbit }} 7 | {{#unless color.$isDisconnected}} 8 | 14 | {{/unless}} 15 | {{/animated-each}} 16 | -------------------------------------------------------------------------------- /app/components/edit-selected-color.hbs: -------------------------------------------------------------------------------- 1 |
2 |
5 | 13 |
14 | 15 |
18 | 27 |
28 |
29 | 38 |
39 |
40 | 49 |
50 |
53 | 61 |
62 |
-------------------------------------------------------------------------------- /app/components/forgot-password.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | import Component from '@glimmer/component'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | import type CognitoService from 'ember-cognito/services/cognito'; 8 | 9 | import 'swach/components/loading-button'; 10 | import type Session from 'swach/services/session'; 11 | 12 | export default class ForgotPasswordComponent extends Component { 13 | @service declare cognito: CognitoService; 14 | @service declare router: Router; 15 | @service declare session: Session; 16 | 17 | @tracked code?: string; 18 | @tracked errorMessage?: string; 19 | @tracked isConfirming = false; 20 | @tracked loading = false; 21 | @tracked password?: string; 22 | @tracked username?: string; 23 | 24 | @action 25 | async forgotPassword(): Promise { 26 | if (this.username) { 27 | this.loading = true; 28 | 29 | try { 30 | await this.cognito.forgotPassword(this.username); 31 | 32 | this.isConfirming = true; 33 | } catch (err: unknown) { 34 | this.errorMessage = (err as Error)?.message; 35 | } finally { 36 | this.loading = false; 37 | } 38 | } 39 | } 40 | 41 | @action 42 | async forgotPasswordSubmit(): Promise { 43 | const { username, code, password } = this; 44 | 45 | if (username && code && password) { 46 | this.loading = true; 47 | 48 | try { 49 | await this.cognito.forgotPasswordSubmit(username, code, password); 50 | 51 | this.router.transitionTo('settings.cloud'); 52 | } catch (err: unknown) { 53 | this.errorMessage = (err as Error)?.message; 54 | } finally { 55 | this.loading = false; 56 | } 57 | } 58 | } 59 | } 60 | 61 | declare module '@glint/environment-ember-loose/registry' { 62 | export default interface Registry { 63 | ForgotPassword: typeof ForgotPasswordComponent; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/components/hex-input.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/hex-input.ts: -------------------------------------------------------------------------------- 1 | import { action, set, setProperties } from '@ember/object'; 2 | import Component from '@glimmer/component'; 3 | 4 | import { TinyColor } from '@ctrl/tinycolor'; 5 | 6 | import type { SelectedColorModel } from 'swach/components/rgb-input'; 7 | import { rgbaToHex } from 'swach/data-models/color'; 8 | 9 | interface HexInputSignature { 10 | Element: HTMLInputElement; 11 | Args: { 12 | selectedColor: SelectedColorModel; 13 | update: (value: string | number) => void; 14 | updateColor: () => void; 15 | value?: string; 16 | }; 17 | } 18 | 19 | export default class HexInputComponent extends Component { 20 | hexRegex = /^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6})$/; 21 | 22 | @action 23 | enterPress(event: KeyboardEvent): void { 24 | if (event.keyCode === 13) { 25 | (event.target).blur(); 26 | } 27 | } 28 | 29 | @action 30 | isComplete(buffer: Buffer, opts: { regex: string }): boolean { 31 | return new RegExp(opts.regex).test(buffer.join('')); 32 | } 33 | 34 | /** 35 | * Executes whenever the hex value matches the regex. We can use this to change the color values only when valid. 36 | * @param {Event} event The event when the hex matches the regex and is valid 37 | */ 38 | @action 39 | onComplete(event: InputEvent): void { 40 | const tinyColor = new TinyColor((event.target).value); 41 | const { r, g, b, a } = tinyColor.toRgb(); 42 | const hex = rgbaToHex(r, g, b, a); 43 | const alpha = parseFloat(a.toFixed(2)); 44 | 45 | setProperties(this.args.selectedColor, { 46 | _hex: hex, 47 | _r: r, 48 | _g: g, 49 | _b: b, 50 | _a: alpha, 51 | hex, 52 | r, 53 | g, 54 | b, 55 | a: alpha, 56 | }); 57 | this.args.updateColor(); 58 | } 59 | 60 | /** 61 | * Resets the hex value if you navigate away 62 | */ 63 | @action 64 | onIncomplete(): void { 65 | set(this.args.selectedColor, '_hex', this.args.selectedColor?.hex); 66 | } 67 | } 68 | 69 | declare module '@glint/environment-ember-loose/registry' { 70 | export default interface Registry { 71 | HexInput: typeof HexInputComponent; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/kuler-palette-row.hbs: -------------------------------------------------------------------------------- 1 |
5 |
6 |
7 | {{@palette.name}} 8 |
9 | 10 |
15 | {{#animated-if this.showMenu use=this.fade}} 16 |
17 | 24 |
25 | {{else}} 26 |
27 | {{svg-jar "more-horizontal" class="icon" height="15" width="15"}} 28 |
29 | {{/animated-if}} 30 |
31 |
32 | 33 |
34 |
35 | {{#each @palette.colors as |color index|}} 36 |
41 |
50 |
55 |
56 | {{/each}} 57 |
58 |
59 |
-------------------------------------------------------------------------------- /app/components/kuler.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 20 |
21 | 22 |
23 |
27 |
28 |
29 | 30 |

31 | Palette 32 |

33 | 34 | {{#if (not (is-empty this.selectedPalette.selectedColorIndex))}} 35 |
36 | 40 | 41 | 45 | 46 | {{#if (not-eq this.selectedPalette.selectedColorIndex 0)}} 47 | 55 | {{/if}} 56 |
57 | {{/if}} -------------------------------------------------------------------------------- /app/components/loading-button.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/loading-button.ts: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | 3 | interface LoadingButtonSignature { 4 | Element: HTMLButtonElement; 5 | Args: { loading: boolean; onClick: () => void }; 6 | Blocks: { 7 | default: []; 8 | }; 9 | } 10 | 11 | // eslint-disable-next-line ember/no-empty-glimmer-component-classes 12 | export default class LoadingButton extends Component {} 13 | 14 | declare module '@glint/environment-ember-loose/registry' { 15 | export default interface Registry { 16 | LoadingButton: typeof LoadingButton; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/components/login.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Sign in 5 |

6 |

7 | or 8 | 12 | sign up free 13 | 14 |

15 |
16 | 17 | {{#if this.errorMessage}} 18 |
19 | {{this.errorMessage}} 20 |
21 | {{/if}} 22 | 23 |
24 |
25 |
26 | 29 | 30 | 41 |
42 |
43 | 46 | 47 | 58 |
59 |
60 | 61 |
62 | 68 | Sign in 69 | 70 |
71 | 72 |
73 |
74 | 78 | Forgot your password? 79 | 80 |
81 |
82 |
83 |
-------------------------------------------------------------------------------- /app/components/login.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | import Component from '@glimmer/component'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | import { storageFor } from 'ember-local-storage'; 8 | 9 | import config from 'swach/config/environment'; 10 | import type Session from 'swach/services/session'; 11 | import type { SettingsStorage } from 'swach/storages/settings'; 12 | 13 | export default class LoginComponent extends Component { 14 | @service declare router: Router; 15 | @service declare session: Session; 16 | 17 | @storageFor('settings') settings!: SettingsStorage; 18 | 19 | @tracked errorMessage?: string; 20 | @tracked loading = false; 21 | @tracked password?: string; 22 | @tracked username?: string; 23 | 24 | @action 25 | async authenticate(): Promise { 26 | this.loading = true; 27 | 28 | const { username, password } = this; 29 | const credentials = { username, password }; 30 | 31 | try { 32 | await this.session.authenticate('authenticator:cognito', credentials); 33 | 34 | // We want to skip this in tests, since once a user has logged in routes become inaccessible 35 | if (config.environment !== 'test') { 36 | this.settings.set('userHasLoggedInBefore', true); 37 | } 38 | 39 | this.router.transitionTo('settings.cloud'); 40 | } catch (error: unknown) { 41 | this.errorMessage = (error as Error).message || (error as string); 42 | } finally { 43 | this.loading = false; 44 | } 45 | } 46 | } 47 | 48 | declare module '@glint/environment-ember-loose/registry' { 49 | export default interface Registry { 50 | Login: typeof LoginComponent; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/components/options-menu.hbs: -------------------------------------------------------------------------------- 1 |
2 | 17 | 18 | {{#if this.isShown}} 19 |
34 | {{yield to="content"}} 35 |
36 | {{/if}} 37 |
-------------------------------------------------------------------------------- /app/components/options-menu.ts: -------------------------------------------------------------------------------- 1 | import type Owner from '@ember/owner'; 2 | import Component from '@glimmer/component'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | interface OptionsMenuSignature { 6 | Element: HTMLDivElement; 7 | Args: { 8 | optionsClasses?: string; 9 | position?: 'left' | 'right'; 10 | showBackground?: boolean; 11 | triggerClasses?: string; 12 | }; 13 | Blocks: { content: []; trigger: [] }; 14 | } 15 | 16 | export default class OptionsMenu extends Component { 17 | @tracked position: 'left' | 'right' = 'right'; 18 | @tracked isShown = false; 19 | 20 | constructor(owner: Owner, args: OptionsMenuSignature['Args']) { 21 | super(owner, args); 22 | 23 | this.position = this.args.position ?? 'right'; 24 | } 25 | } 26 | 27 | declare module '@glint/environment-ember-loose/registry' { 28 | export default interface Registry { 29 | OptionsMenu: typeof OptionsMenu; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/palettes-list.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if this.palettes.length}} 3 | 10 | {{! TODO: remove this disconnected check when caching is fixed in ember-orbit }} 11 | {{#unless palette.$isDisconnected}} 12 | 16 | {{/unless}} 17 | 18 | {{else}} 19 | {{#if @showFavorites}} 20 |
21 |
22 | {{svg-jar "alert-circle" height="50" width="50"}} 23 |
24 | 25 |

26 | No Favorites 27 |

28 | 29 |

30 | You can favorite a palette by clicking the 31 | {{svg-jar "filled-heart" class="inline" height="14" width="14"}} 32 | icon in the palette's menu. 33 |

34 |
35 | {{/if}} 36 | {{/if}} 37 |
-------------------------------------------------------------------------------- /app/components/palettes-list.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import { service } from '@ember/service'; 3 | import Component from '@glimmer/component'; 4 | 5 | import type { LiveQuery, Store } from 'ember-orbit'; 6 | 7 | import type { RecordOperationTerm } from '@orbit/records'; 8 | 9 | import type ColorModel from 'swach/data-models/color'; 10 | import type PaletteModel from 'swach/data-models/palette'; 11 | import type UndoManager from 'swach/services/undo-manager'; 12 | 13 | interface PalettesListSignature { 14 | Element: HTMLDivElement; 15 | Args: { 16 | palettes: LiveQuery; 17 | moveColorsBetweenPalettes: ({ 18 | sourceArgs, 19 | sourceList, 20 | sourceIndex, 21 | targetArgs, 22 | targetList, 23 | targetIndex, 24 | }: { 25 | sourceArgs: { isColorHistory: boolean; parent: PaletteModel }; 26 | sourceList: ColorModel[]; 27 | sourceIndex: number; 28 | targetArgs: { isColorHistory: boolean; parent: PaletteModel }; 29 | targetList: ColorModel[]; 30 | targetIndex: number; 31 | }) => Promise; 32 | showFavorites: boolean; 33 | }; 34 | } 35 | 36 | export default class PalettesListComponent extends Component { 37 | @service declare store: Store; 38 | @service declare undoManager: UndoManager; 39 | 40 | get palettes(): PaletteModel[] { 41 | const palettes = (this.args.palettes?.value ?? []) as PaletteModel[]; 42 | 43 | if (this.args.showFavorites) { 44 | return palettes.filter((palette) => palette.isFavorite); 45 | } 46 | 47 | return palettes; 48 | } 49 | 50 | @action 51 | async reorderPalettes({ 52 | sourceList, 53 | sourceIndex, 54 | targetList, 55 | targetIndex, 56 | }: { 57 | sourceList: PaletteModel[]; 58 | sourceIndex: number; 59 | targetList: PaletteModel[]; 60 | targetIndex: number; 61 | }): Promise { 62 | if (sourceList === targetList && sourceIndex === targetIndex) return; 63 | 64 | const movedItem = sourceList[sourceIndex] as PaletteModel; 65 | 66 | sourceList.splice(sourceIndex, 1); 67 | targetList.splice(targetIndex, 0, movedItem); 68 | 69 | await this.store.update((t) => { 70 | const operations: RecordOperationTerm[] = []; 71 | 72 | targetList.forEach((palette: PaletteModel, index: number) => { 73 | operations.push( 74 | t.replaceAttribute( 75 | { type: 'palette', id: palette.id }, 76 | 'index', 77 | index, 78 | ), 79 | ); 80 | }); 81 | 82 | return operations; 83 | }); 84 | 85 | this.undoManager.setupUndoRedo(); 86 | } 87 | } 88 | 89 | declare module '@glint/environment-ember-loose/registry' { 90 | export default interface Registry { 91 | PalettesList: typeof PalettesListComponent; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/components/register-confirm.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Confirm Registration 5 |

6 |
7 | 8 | {{#if this.errorMessage}} 9 |
10 | {{this.errorMessage}} 11 |
12 | {{/if}} 13 | 14 |
15 |
16 |
17 | 20 | 21 | 32 |
33 | 34 |
35 | 38 | 39 | 49 |
50 |
51 | 52 | {{! TODO: https://app.clickup.com/t/hcw0ey
53 |
54 | 55 | Resend confirmation code 56 | 57 |
58 |
}} 59 | 60 |
61 | 69 |
70 |
71 |
-------------------------------------------------------------------------------- /app/components/register-confirm.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | import Component from '@glimmer/component'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | import type CognitoService from 'ember-cognito/services/cognito'; 8 | 9 | export default class RegisterConfirm extends Component { 10 | @service declare cognito: CognitoService; 11 | @service declare router: Router; 12 | 13 | @tracked errorMessage?: string; 14 | @tracked code?: string; 15 | @tracked username?: string; 16 | 17 | @action 18 | async confirm(): Promise { 19 | const { username, code } = this; 20 | 21 | if (username && code) { 22 | try { 23 | await this.cognito.confirmSignUp(username, code); 24 | 25 | this.router.transitionTo('settings.cloud'); 26 | } catch (err: unknown) { 27 | this.errorMessage = (err as Error)?.message; 28 | } 29 | } 30 | } 31 | } 32 | 33 | declare module '@glint/environment-ember-loose/registry' { 34 | export default interface Registry { 35 | RegisterConfirm: typeof RegisterConfirm; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/components/register.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | Sign up 5 |

6 |

7 | or 8 | 12 | sign in 13 | 14 |

15 |
16 | 17 | {{#if this.errorMessage}} 18 |
19 | {{this.errorMessage}} 20 |
21 | {{/if}} 22 | 23 |
24 | 25 |
26 |
27 | 30 | 31 | 42 |
43 |
44 | 47 | 48 | 59 |
60 |
61 | 62 |
63 | 71 |
72 |
73 |
-------------------------------------------------------------------------------- /app/components/register.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | import Component from '@glimmer/component'; 5 | import { tracked } from '@glimmer/tracking'; 6 | 7 | import type CognitoService from 'ember-cognito/services/cognito'; 8 | 9 | export default class RegisterComponent extends Component { 10 | @service declare cognito: CognitoService; 11 | @service declare router: Router; 12 | 13 | @tracked errorMessage?: string; 14 | @tracked password?: string; 15 | @tracked username?: string; 16 | 17 | @action 18 | async register(): Promise { 19 | const { username, password } = this; 20 | 21 | if (username && password) { 22 | const attributes = { 23 | email: username, 24 | }; 25 | 26 | try { 27 | await this.cognito.signUp(username, password, attributes); 28 | 29 | this.router.transitionTo('settings.cloud.register.confirm'); 30 | } catch (err: unknown) { 31 | this.errorMessage = (err as Error)?.message; 32 | } 33 | } 34 | } 35 | } 36 | 37 | declare module '@glint/environment-ember-loose/registry' { 38 | export default interface Registry { 39 | Register: typeof RegisterComponent; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/components/rgb-input.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{@type}}: 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/components/settings-data.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | Formats 4 |
5 | 6 |

7 | This determines the default color format that gets copied to the clipboard. 8 |

9 | 10 | 16 | <:trigger> 17 | {{this.settings.defaultColorFormat}} 18 | 19 |
20 | {{svg-jar "chevron-left" class="h-3 w-3 -rotate-90"}} 21 |
22 | 23 | <:content> 24 | {{#each this.colorFormats as |format|}} 25 | 33 | {{/each}} 34 | 35 |
36 | 37 |
38 |
39 | Data management 40 |
41 | 42 | 48 | Backup swatches 49 | 50 | 51 | 56 | Restore from backup 57 | 58 |
59 |
-------------------------------------------------------------------------------- /app/components/settings-menu.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@ember/object'; 2 | import type Owner from '@ember/owner'; 3 | import Component from '@glimmer/component'; 4 | import { tracked } from '@glimmer/tracking'; 5 | 6 | import { storageFor } from 'ember-local-storage'; 7 | 8 | import type { IpcRenderer } from 'electron'; 9 | 10 | import type { SettingsStorage, themes } from 'swach/storages/settings'; 11 | 12 | interface SettingsMenuSignature { 13 | Element: HTMLDivElement; 14 | Args: { 15 | checkForUpdates: () => void; 16 | enableDisableAutoStart: (e: InputEvent) => void; 17 | toggleShowDockIcon: (e: InputEvent) => void; 18 | }; 19 | } 20 | 21 | export default class SettingsMenu extends Component { 22 | @storageFor('settings') settings!: SettingsStorage; 23 | 24 | declare ipcRenderer: IpcRenderer; 25 | themes = ['light', 'dark', 'dynamic']; 26 | 27 | @tracked platform?: string; 28 | 29 | constructor(owner: Owner, args: SettingsMenuSignature['Args']) { 30 | super(owner, args); 31 | 32 | if (typeof requireNode !== 'undefined') { 33 | const { ipcRenderer } = requireNode('electron'); 34 | 35 | this.ipcRenderer = ipcRenderer; 36 | 37 | void this.ipcRenderer.invoke('getPlatform').then((platform: string) => { 38 | this.platform = platform; 39 | }); 40 | } 41 | } 42 | 43 | get isMacOS() { 44 | return this.platform === 'darwin'; 45 | } 46 | 47 | get isMacOSOrWindows() { 48 | return this.platform === 'darwin' || this.platform === 'win32'; 49 | } 50 | 51 | @action 52 | changeTheme(theme: themes): void { 53 | this.settings.set('userTheme', theme); 54 | } 55 | } 56 | 57 | declare module '@glint/environment-ember-loose/registry' { 58 | export default interface Registry { 59 | SettingsMenu: typeof SettingsMenu; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/components/settings-nav.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type declarations for 3 | * import config from 'swach/config/environment' 4 | */ 5 | declare const config: { 6 | environment: string; 7 | modulePrefix: string; 8 | podModulePrefix: string; 9 | locationType: 'history' | 'hash' | 'none'; 10 | rootURL: string; 11 | APP: Record; 12 | SCHEMA_VERSION: number; 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /app/controllers/colors.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import type ApplicationController from 'swach/controllers/application'; 4 | import type PaletteModel from 'swach/data-models/palette'; 5 | 6 | export default class ColorsController extends Controller { 7 | queryParams = ['paletteId']; 8 | 9 | @controller application!: ApplicationController; 10 | 11 | declare model: PaletteModel; 12 | 13 | paletteId = null; 14 | } 15 | 16 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 17 | declare module '@ember/controller' { 18 | interface Registry { 19 | colors: ColorsController; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/controllers/contrast.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | 4 | export default class ContrastController extends Controller { 5 | @action 6 | goBack(): void { 7 | window.history.back(); 8 | } 9 | } 10 | 11 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 12 | declare module '@ember/controller' { 13 | interface Registry { 14 | contrast: ContrastController; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/kuler.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | 4 | import type ColorModel from 'swach/data-models/color'; 5 | 6 | export default class KulerController extends Controller { 7 | queryParams = ['colorId']; 8 | 9 | colorId = null; 10 | declare model: ColorModel; 11 | 12 | @action 13 | goBack(): void { 14 | window.history.back(); 15 | } 16 | } 17 | 18 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 19 | declare module '@ember/controller' { 20 | interface Registry { 21 | kuler: KulerController; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/controllers/settings.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { service } from '@ember/service'; 4 | 5 | import type ApplicationController from 'swach/controllers/application'; 6 | import type Session from 'swach/services/session'; 7 | 8 | export default class SettingsController extends Controller { 9 | @controller application!: ApplicationController; 10 | @service declare session: Session; 11 | 12 | @action 13 | goBack(): void { 14 | window.history.back(); 15 | } 16 | } 17 | 18 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 19 | declare module '@ember/controller' { 20 | interface Registry { 21 | settings: SettingsController; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/controllers/settings/cloud/profile.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { service } from '@ember/service'; 4 | import { tracked } from '@glimmer/tracking'; 5 | 6 | import type Session from 'swach/services/session'; 7 | 8 | export default class SettingsAccountController extends Controller { 9 | @service declare session: Session; 10 | 11 | @tracked loading = false; 12 | 13 | @action 14 | logOut() { 15 | this.loading = true; 16 | this.session.invalidate(); 17 | this.loading = false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/controllers/settings/index.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import type ApplicationController from 'swach/controllers/application'; 4 | 5 | export default class SettingsIndexController extends Controller { 6 | @controller application!: ApplicationController; 7 | } 8 | -------------------------------------------------------------------------------- /app/controllers/welcome.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import { storageFor } from 'ember-local-storage'; 4 | 5 | import type ApplicationController from 'swach/controllers/application'; 6 | import type { SettingsStorage } from 'swach/storages/settings'; 7 | 8 | export default class WelcomeController extends Controller { 9 | @controller application!: ApplicationController; 10 | 11 | @storageFor('settings') settings!: SettingsStorage; 12 | } 13 | 14 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 15 | declare module '@ember/controller' { 16 | interface Registry { 17 | welcome: WelcomeController; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/controllers/welcome/auto-start.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import { storageFor } from 'ember-local-storage'; 4 | 5 | import 'swach/components/toggle-switch'; 6 | import type ApplicationController from 'swach/controllers/application'; 7 | import type { SettingsStorage } from 'swach/storages/settings'; 8 | 9 | export default class WelcomeAutoStartController extends Controller { 10 | @controller application!: ApplicationController; 11 | 12 | @storageFor('settings') settings!: SettingsStorage; 13 | } 14 | -------------------------------------------------------------------------------- /app/controllers/welcome/dock-icon.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import { storageFor } from 'ember-local-storage'; 4 | 5 | import type ApplicationController from 'swach/controllers/application'; 6 | import type { SettingsStorage } from 'swach/storages/settings'; 7 | 8 | export default class WelcomeDockIconController extends Controller { 9 | @controller application!: ApplicationController; 10 | 11 | @storageFor('settings') settings!: SettingsStorage; 12 | } 13 | -------------------------------------------------------------------------------- /app/controllers/welcome/index.ts: -------------------------------------------------------------------------------- 1 | import Controller, { inject as controller } from '@ember/controller'; 2 | 3 | import { storageFor } from 'ember-local-storage'; 4 | 5 | import type ApplicationController from 'swach/controllers/application'; 6 | import type { SettingsStorage } from 'swach/storages/settings'; 7 | 8 | export default class WelcomeIndexController extends Controller { 9 | @controller application!: ApplicationController; 10 | 11 | @storageFor('settings') settings!: SettingsStorage; 12 | } 13 | -------------------------------------------------------------------------------- /app/data-buckets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/app/data-buckets/.gitkeep -------------------------------------------------------------------------------- /app/data-buckets/main.js: -------------------------------------------------------------------------------- 1 | import BucketClass from '@orbit/indexeddb-bucket'; 2 | 3 | export default { 4 | create(injections = {}) { 5 | injections.name = 'main'; 6 | injections.namespace = 'swach-main'; 7 | 8 | return new BucketClass(injections); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /app/data-models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/app/data-models/.gitkeep -------------------------------------------------------------------------------- /app/data-models/color.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from '@ember/utils'; 2 | 3 | import { Model, attr, hasOne } from 'ember-orbit'; 4 | 5 | import { TinyColor } from '@ctrl/tinycolor'; 6 | 7 | import type PaletteModel from 'swach/data-models/palette'; 8 | 9 | export default class ColorModel extends Model { 10 | @attr('datetime') createdAt!: string; 11 | @attr('string') name!: string; 12 | @attr('number') r!: number; 13 | @attr('number') g!: number; 14 | @attr('number') b!: number; 15 | @attr('number') a!: number; 16 | 17 | @hasOne('palette', { inverse: 'colors' }) palette!: PaletteModel; 18 | 19 | get hex(): string { 20 | const { r, g, b, a } = this; 21 | 22 | return rgbaToHex(r, g, b, a); 23 | } 24 | 25 | get hsl(): string { 26 | const { r, g, b, a } = this; 27 | 28 | return new TinyColor({ r, g, b, a }).toHslString(); 29 | } 30 | 31 | get rgba(): string { 32 | const { r, g, b, a } = this; 33 | 34 | return new TinyColor({ r, g, b, a }).toRgbString(); 35 | } 36 | } 37 | 38 | export function rgbaToHex(r: number, g: number, b: number, a: number): string { 39 | if (isEmpty(a) || a === 1) { 40 | return new TinyColor({ r, g, b, a }).toHexString(); 41 | } 42 | 43 | return new TinyColor({ r, g, b, a }).toHex8String(); 44 | } 45 | -------------------------------------------------------------------------------- /app/data-models/palette.ts: -------------------------------------------------------------------------------- 1 | import { Model, attr, hasMany } from 'ember-orbit'; 2 | 3 | import type ColorModel from 'swach/data-models/color'; 4 | 5 | export default class PaletteModel extends Model { 6 | @attr('datetime') createdAt!: string; 7 | @attr('number') index!: number; 8 | @attr('boolean') isColorHistory!: boolean; 9 | @attr('boolean') isFavorite!: boolean; 10 | @attr('boolean') isLocked!: boolean; 11 | @attr('string') name!: string; 12 | @attr('number') selectedColorIndex!: number; 13 | // This is an array to track color order, and is a hack until orbit supports ordered relationships 14 | @attr('array') colorOrder!: { type: string; id: string }[]; 15 | 16 | @hasMany('color', { inverse: 'palette', dependent: 'remove' }) 17 | colors!: ColorModel[]; 18 | } 19 | -------------------------------------------------------------------------------- /app/data-sources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/app/data-sources/.gitkeep -------------------------------------------------------------------------------- /app/data-strategies/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/app/data-strategies/.gitkeep -------------------------------------------------------------------------------- /app/data-strategies/event-logging.js: -------------------------------------------------------------------------------- 1 | import { EventLoggingStrategy } from '@orbit/coordinator'; 2 | 3 | import config from 'swach/config/environment'; 4 | 5 | const factory = { 6 | create() { 7 | return new EventLoggingStrategy(); 8 | }, 9 | }; 10 | 11 | export default config.environment === 'development' ? factory : null; 12 | -------------------------------------------------------------------------------- /app/data-strategies/remote-queryfail.js: -------------------------------------------------------------------------------- 1 | import { RequestStrategy } from '@orbit/coordinator'; 2 | 3 | export default { 4 | create() { 5 | return new RequestStrategy({ 6 | name: 'remote-queryfail', 7 | source: 'remote', 8 | on: 'queryFail', 9 | action() { 10 | // Skip failed remote queries since there's no need to retain them in 11 | // the queue for later retries (unlike updates). 12 | // TODO: Consider logging errors in sentry or equivalent. 13 | this.source.requestQueue.skip(); 14 | }, 15 | }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /app/data-strategies/remote-store-sync.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '@ember/application'; 2 | 3 | import { SyncStrategy } from '@orbit/coordinator'; 4 | 5 | export default { 6 | create(injections = {}) { 7 | const app = getOwner(injections); 8 | const session = app.lookup('service:session'); 9 | 10 | return new SyncStrategy({ 11 | name: 'remote-store-sync', 12 | 13 | /** 14 | * The name of the source which will have its `transform` event observed. 15 | */ 16 | source: 'remote', 17 | 18 | /** 19 | * The name of the source which will be acted upon. 20 | * 21 | * When the source receives the `transform` event, the `sync` method 22 | * will be invoked on the target. 23 | */ 24 | target: 'store', 25 | 26 | /** 27 | * A handler for any errors thrown as a result of invoking `sync` on the 28 | * target. 29 | */ 30 | // catch(e) {}, 31 | 32 | /** 33 | * A filter function that returns `true` if `sync` should be performed. 34 | * 35 | * `filter` will be invoked in the context of this strategy (and thus will 36 | * have access to both `this.source` and `this.target`). 37 | */ 38 | filter() { 39 | // only sync remote if authenticated 40 | return session.isAuthenticated; 41 | }, 42 | 43 | /** 44 | * Ensure that remote transforms are sync'd with the store before 45 | * remote requests resolve. 46 | */ 47 | blocking: true, 48 | }); 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /app/data-strategies/remote-updatefail.js: -------------------------------------------------------------------------------- 1 | import { RequestStrategy } from '@orbit/coordinator'; 2 | import { NetworkError } from '@orbit/jsonapi'; 3 | 4 | export default { 5 | create() { 6 | return new RequestStrategy({ 7 | name: 'remote-updatefail', 8 | source: 'remote', 9 | on: 'updateFail', 10 | action(transform, e) { 11 | // Retry network / fetch failures 12 | if (e instanceof NetworkError || e.message === 'Failed to fetch') { 13 | // TODO: Consider an incremental backoff rather than a fixed delay. 14 | setTimeout(() => { 15 | this.source.requestQueue.retry(); 16 | }, 5000); 17 | } else { 18 | // TODO: Consider logging additional errors with sentry or equivalent. 19 | } 20 | }, 21 | }); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /app/data-strategies/store-backup-sync.ts: -------------------------------------------------------------------------------- 1 | import { SyncStrategy } from '@orbit/coordinator'; 2 | 3 | export default { 4 | create(): SyncStrategy { 5 | return new SyncStrategy({ 6 | name: 'store-backup-sync', 7 | 8 | /** 9 | * The name of the source which will have its `transform` event observed. 10 | */ 11 | source: 'store', 12 | 13 | /** 14 | * The name of the source which will be acted upon. 15 | * 16 | * When the source receives the `transform` event, the `sync` method 17 | * will be invoked on the target. 18 | */ 19 | target: 'backup', 20 | 21 | /** 22 | * A handler for any errors thrown as a result of invoking `sync` on the 23 | * target. 24 | */ 25 | // catch(e) {}, 26 | 27 | /** 28 | * A filter function that returns `true` if `sync` should be performed. 29 | * 30 | * `filter` will be invoked in the context of this strategy (and thus will 31 | * have access to both `this.source` and `this.target`). 32 | */ 33 | // filter(...args) {}; 34 | 35 | /** 36 | * Should resolution of the target's `sync` block the completion of the 37 | * source's `transform`? 38 | * 39 | * Can be specified as a boolean or a function which which will be 40 | * invoked in the context of this strategy (and thus will have access to 41 | * both `this.source` and `this.target`). 42 | */ 43 | blocking: true, 44 | }); 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /app/data-strategies/store-beforeupdate-remote-update.js: -------------------------------------------------------------------------------- 1 | import { getOwner } from '@ember/application'; 2 | 3 | import { RequestStrategy } from '@orbit/coordinator'; 4 | 5 | export default { 6 | create(injections = {}) { 7 | const app = getOwner(injections); 8 | const session = app.lookup('service:session'); 9 | 10 | return new RequestStrategy({ 11 | name: 'store-beforeupdate-remote-update', 12 | 13 | /** 14 | * The name of the source to be observed. 15 | */ 16 | source: 'store', 17 | 18 | /** 19 | * The name of the event to observe (e.g. `beforeQuery`, `query`, 20 | * `beforeUpdate`, `update`, etc.). 21 | */ 22 | on: 'beforeUpdate', 23 | 24 | /** 25 | * The name of the source which will be acted upon. 26 | */ 27 | target: 'remote', 28 | 29 | /** 30 | * The action to perform on the target. 31 | * 32 | * Can be specified as a string (e.g. `pull`) or a function which will be 33 | * invoked in the context of this strategy (and thus will have access to 34 | * both `this.source` and `this.target`). 35 | */ 36 | action: 'update', 37 | 38 | /** 39 | * A handler for any errors thrown as a result of performing the action. 40 | */ 41 | // catch(e) {}, 42 | 43 | /** 44 | * A filter function that returns `true` if the `action` should be performed. 45 | * 46 | * `filter` will be invoked in the context of this strategy (and thus will 47 | * have access to both `this.source` and `this.target`). 48 | */ 49 | filter() { 50 | // only update remote if authenticated 51 | return session.isAuthenticated; 52 | }, 53 | 54 | /** 55 | * Should results returned from calling `action` on the `target` source be 56 | * passed as hint data back to the `source`? 57 | * 58 | * This can allow hints to inform the processing of subsequent actions on the 59 | * source. For instance, a `beforeQuery` event might invoke `query` on a 60 | * target, and those results could inform how the originating source performs 61 | * `_query`. This might allow a target source's sorting and filtering of 62 | * results to affect how the originating source processes the query. 63 | * 64 | * This setting is only effective for `blocking` strategies, since only in 65 | * those scenarios is processing delayed. 66 | */ 67 | passHints: false, 68 | 69 | /** 70 | * Don't block fulfillment of requests to the store with associated remote 71 | * requests. Allows for optimistic UX. 72 | */ 73 | blocking: false, 74 | }); 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /app/deprecation-workflow.ts: -------------------------------------------------------------------------------- 1 | import setupDeprecationWorkflow from 'ember-cli-deprecation-workflow'; 2 | 3 | /** 4 | * Docs: https://github.com/ember-cli/ember-cli-deprecation-workflow 5 | */ 6 | setupDeprecationWorkflow({ 7 | /** 8 | false by default, but if a developer / team wants to be more aggressive about being proactive with 9 | handling their deprecations, this should be set to "true" 10 | */ 11 | throwOnUnhandled: false, 12 | workflow: [ 13 | { handler: 'silence', matchId: 'ember-simple-auth.events.session-service' }, 14 | { handler: 'silence', matchId: 'ember-polyfills.deprecate-assign' }, 15 | { handler: 'silence', matchId: 'ember-string.htmlsafe-ishtmlsafe' }, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /app/helpers/capitalize.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@ember/component/helper'; 2 | import { capitalize as _capitalize } from '@ember/string'; 3 | 4 | export function capitalize([string]: [string]): string { 5 | return _capitalize(string); 6 | } 7 | 8 | export default helper(capitalize); 9 | -------------------------------------------------------------------------------- /app/helpers/html-safe.ts: -------------------------------------------------------------------------------- 1 | import { helper } from '@ember/component/helper'; 2 | import { htmlSafe as _htmlSafe } from '@ember/template'; 3 | 4 | import type { SafeString } from 'ember__template/-private/handlebars'; 5 | 6 | export function htmlSafe([string]: [string]): SafeString { 7 | return _htmlSafe(string); 8 | } 9 | 10 | export default helper(htmlSafe); 11 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swach 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/initializers/main-bucket-initializer.js: -------------------------------------------------------------------------------- 1 | import BucketFactory from '../data-buckets/main'; 2 | 3 | export function initialize(application) { 4 | const orbitConfig = application.resolveRegistration('ember-orbit:config'); 5 | 6 | application.register(`service:${orbitConfig.services.bucket}`, BucketFactory); 7 | } 8 | 9 | export default { 10 | name: 'main-bucket-initializer', 11 | after: 'ember-orbit-config', 12 | initialize, 13 | }; 14 | -------------------------------------------------------------------------------- /app/router.ts: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | 3 | import config from 'swach/config/environment'; 4 | 5 | export default class Router extends EmberRouter { 6 | location = config.locationType; 7 | rootURL = config.rootURL; 8 | } 9 | 10 | Router.map(function () { 11 | this.route('colors'); 12 | this.route('contrast'); 13 | this.route('kuler'); 14 | this.route('palettes'); 15 | this.route('settings', function () { 16 | this.route('cloud', function () { 17 | this.route('forgot-password'); 18 | this.route('login'); 19 | this.route('profile'); 20 | this.route('register', function () { 21 | this.route('confirm'); 22 | this.route('resend'); 23 | }); 24 | }); 25 | this.route('data'); 26 | }); 27 | this.route('welcome', function () { 28 | this.route('auto-start'); 29 | this.route('cloud-sync'); 30 | this.route('dock-icon'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/routes/-private/application.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | 5 | import type DataService from 'swach/services/data'; 6 | import type Session from 'swach/services/session'; 7 | 8 | export default class ApplicationRoute extends Route { 9 | @service declare data: DataService; 10 | @service declare router: Router; 11 | @service declare session: Session; 12 | 13 | async beforeModel(): Promise { 14 | await this.session.setup(); 15 | 16 | await this.data.activate(); 17 | await this.data.synchronize(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | // This is a hack because ember-simple-auth exports an application.js and causes collisions 2 | export { default } from 'swach/routes/-private/application'; 3 | -------------------------------------------------------------------------------- /app/routes/colors.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | import type { Store } from 'ember-orbit'; 7 | 8 | import type PaletteModel from 'swach/data-models/palette'; 9 | import type Session from 'swach/services/session'; 10 | import type { SettingsStorage } from 'swach/storages/settings'; 11 | import viewTransitions from 'swach/utils/view-transitions'; 12 | 13 | export default class ColorsRoute extends Route { 14 | queryParams = { 15 | paletteId: { 16 | refreshModel: true, 17 | }, 18 | }; 19 | 20 | @service declare session: Session; 21 | @service declare store: Store; 22 | 23 | @storageFor('settings') settings!: SettingsStorage; 24 | 25 | beforeModel(transition: Transition): void { 26 | if (this.settings.get('userHasLoggedInBefore')) { 27 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 28 | } 29 | } 30 | 31 | async model({ 32 | paletteId, 33 | }: { 34 | paletteId: string; 35 | }): Promise { 36 | if (paletteId) { 37 | const palette = await this.store.findRecord('palette', paletteId); 38 | 39 | return palette; 40 | } 41 | } 42 | 43 | async afterModel() { 44 | await viewTransitions(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/routes/contrast.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | 7 | import type Session from 'swach/services/session'; 8 | import type { SettingsStorage } from 'swach/storages/settings'; 9 | import viewTransitions from 'swach/utils/view-transitions'; 10 | 11 | export default class ContrastRoute extends Route { 12 | @service declare session: Session; 13 | 14 | @storageFor('settings') settings!: SettingsStorage; 15 | 16 | beforeModel(transition: Transition): void { 17 | if (this.settings.get('userHasLoggedInBefore')) { 18 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 19 | } 20 | } 21 | 22 | async afterModel() { 23 | await viewTransitions(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/routes/index.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Router from '@ember/routing/router-service'; 3 | import { service } from '@ember/service'; 4 | 5 | export default class IndexRoute extends Route { 6 | @service declare router: Router; 7 | 8 | beforeModel(): void { 9 | this.router.replaceWith('palettes'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/routes/kuler.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | import type { Store } from 'ember-orbit'; 7 | 8 | import type ColorModel from 'swach/data-models/color'; 9 | import type Session from 'swach/services/session'; 10 | import type { SettingsStorage } from 'swach/storages/settings'; 11 | import viewTransitions from 'swach/utils/view-transitions'; 12 | 13 | export default class KulerRoute extends Route { 14 | queryParams = { 15 | colorId: { 16 | refreshModel: true, 17 | }, 18 | }; 19 | 20 | @service declare session: Session; 21 | @service declare store: Store; 22 | 23 | @storageFor('settings') settings!: SettingsStorage; 24 | 25 | beforeModel(transition: Transition): void { 26 | if (this.settings.get('userHasLoggedInBefore')) { 27 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 28 | } 29 | } 30 | 31 | async model({ 32 | colorId, 33 | }: { 34 | colorId: string; 35 | }): Promise { 36 | if (colorId) { 37 | const color = await this.store.findRecord('color', colorId); 38 | 39 | return color; 40 | } 41 | } 42 | 43 | async afterModel() { 44 | await viewTransitions(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/routes/palettes.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | import type { LiveQuery, Store } from 'ember-orbit'; 7 | 8 | import type Session from 'swach/services/session'; 9 | import type { SettingsStorage } from 'swach/storages/settings'; 10 | import viewTransitions from 'swach/utils/view-transitions'; 11 | 12 | export default class PalettesRoute extends Route { 13 | @service declare session: Session; 14 | @service declare store: Store; 15 | 16 | @storageFor('settings') settings!: SettingsStorage; 17 | 18 | beforeModel(transition: Transition): void { 19 | if (this.settings.get('userHasLoggedInBefore')) { 20 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 21 | } 22 | } 23 | 24 | model(): LiveQuery { 25 | return this.store.cache.liveQuery((qb) => 26 | qb 27 | .findRecords('palette') 28 | .filter({ attribute: 'isColorHistory', value: false }) 29 | .sort('index'), 30 | ); 31 | } 32 | 33 | async afterModel() { 34 | await viewTransitions(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/routes/settings/cloud/index.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Router from '@ember/routing/router-service'; 3 | import type Transition from '@ember/routing/transition'; 4 | import { service } from '@ember/service'; 5 | 6 | import type CognitoService from 'ember-cognito/services/cognito'; 7 | 8 | import type Session from 'swach/services/session'; 9 | import viewTransitions from 'swach/utils/view-transitions'; 10 | 11 | export default class SettingsAccountRoute extends Route { 12 | @service declare cognito: CognitoService; 13 | @service declare router: Router; 14 | @service declare session: Session; 15 | 16 | async beforeModel(transition: Transition): Promise { 17 | if (!this.session.isAuthenticated) { 18 | transition.abort(); 19 | 20 | return this.router.transitionTo('settings.cloud.login'); 21 | } 22 | 23 | return this.router.transitionTo('settings.cloud.profile'); 24 | } 25 | 26 | async afterModel() { 27 | await viewTransitions(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/routes/settings/cloud/profile.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import { service } from '@ember/service'; 3 | 4 | import type CognitoService from 'ember-cognito/services/cognito'; 5 | 6 | import type Session from 'swach/services/session'; 7 | import viewTransitions from 'swach/utils/view-transitions'; 8 | 9 | export default class SettingsAccountRoute extends Route { 10 | @service declare cognito: CognitoService; 11 | @service declare session: Session; 12 | 13 | model(): CognitoService['user']['attributes'] { 14 | console.log(this.cognito); 15 | 16 | return this.cognito.user?.attributes; 17 | } 18 | 19 | async afterModel() { 20 | await viewTransitions(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/routes/settings/data.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | 7 | import type Session from 'swach/services/session'; 8 | import type { SettingsStorage } from 'swach/storages/settings'; 9 | import viewTransitions from 'swach/utils/view-transitions'; 10 | 11 | export default class SettingsDataRoute extends Route { 12 | @service declare session: Session; 13 | 14 | @storageFor('settings') settings!: SettingsStorage; 15 | 16 | beforeModel(transition: Transition): void { 17 | if (this.settings.get('userHasLoggedInBefore')) { 18 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 19 | } 20 | } 21 | 22 | async afterModel() { 23 | await viewTransitions(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/routes/settings/index.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import type Transition from '@ember/routing/transition'; 3 | import { service } from '@ember/service'; 4 | 5 | import { storageFor } from 'ember-local-storage'; 6 | 7 | import type Session from 'swach/services/session'; 8 | import type { SettingsStorage } from 'swach/storages/settings'; 9 | import viewTransitions from 'swach/utils/view-transitions'; 10 | 11 | export default class SettingsIndexRoute extends Route { 12 | @service declare session: Session; 13 | 14 | @storageFor('settings') settings!: SettingsStorage; 15 | 16 | beforeModel(transition: Transition): void { 17 | if (this.settings.get('userHasLoggedInBefore')) { 18 | this.session.requireAuthentication(transition, 'settings.cloud.login'); 19 | } 20 | } 21 | 22 | async afterModel() { 23 | await viewTransitions(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/services/cognito.js: -------------------------------------------------------------------------------- 1 | import { service } from '@ember/service'; 2 | 3 | import CognitoService from 'ember-cognito/services/cognito'; 4 | 5 | import ENV from '../config/environment'; 6 | 7 | const cognitoEnv = Object.assign( 8 | { 9 | autoRefreshSession: false, 10 | }, 11 | ENV.cognito, 12 | ); 13 | 14 | export default class CognitoServiceExtended extends CognitoService { 15 | @service session; 16 | poolId = cognitoEnv.poolId; 17 | clientId = cognitoEnv.clientId; 18 | identityPoolId = cognitoEnv.identityPoolId; 19 | region = cognitoEnv.region; 20 | autoRefreshSession = cognitoEnv.autoRefreshSession; 21 | authenticationFlowType = cognitoEnv.authenticationFlowType; 22 | 23 | /** 24 | * Configures the Amplify library with the pool & client IDs, and any additional 25 | * configuration. 26 | * @param awsconfig Extra AWS configuration. 27 | */ 28 | configure(awsconfig) { 29 | const { poolId, clientId, region, identityPoolId } = this; 30 | const params = Object.assign( 31 | { 32 | identityPoolId: identityPoolId, 33 | region: region, 34 | userPoolId: poolId, 35 | userPoolWebClientId: clientId, 36 | }, 37 | awsconfig, 38 | ); 39 | 40 | this.auth.configure(params); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/services/nearest-color.ts: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | 3 | import colorNameList from 'color-name-list'; 4 | import nearestColor from 'nearest-color'; 5 | 6 | export default class NearestColorService extends Service { 7 | nearest: ({ r, g, b }: { r: number; g: number; b: number }) => { 8 | name: string; 9 | }; 10 | 11 | constructor() { 12 | super(...arguments); 13 | 14 | const namedColors = colorNameList.reduce( 15 | ( 16 | o: { [key: string]: string }, 17 | { name, hex }: { name: string; hex: string }, 18 | ) => Object.assign(o, { [name]: hex }), 19 | {}, 20 | ); 21 | 22 | this.nearest = nearestColor.from(namedColors); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/services/session.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@ember/service'; 2 | 3 | import BaseSessionService from 'ember-simple-auth/services/session'; 4 | 5 | import DataService from './data'; 6 | 7 | interface Data { 8 | authenticated: { 9 | id: string; 10 | }; 11 | } 12 | 13 | export default class SessionService extends BaseSessionService { 14 | @service('data') declare swachData: DataService; 15 | 16 | handleAuthentication(routeAfterAuthentication: string) { 17 | super.handleAuthentication(routeAfterAuthentication); 18 | 19 | void this.swachData.synchronize(); 20 | } 21 | 22 | handleInvalidation(routeAfterInvalidation: string) { 23 | super.handleInvalidation(routeAfterInvalidation); 24 | 25 | void this.swachData.reset(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/session-stores/application.ts: -------------------------------------------------------------------------------- 1 | import Adaptive from 'ember-simple-auth/session-stores/adaptive'; 2 | 3 | export default Adaptive; 4 | -------------------------------------------------------------------------------- /app/storages/settings.ts: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | export type themes = 'dynamic' | 'light' | 'dark'; 4 | 5 | interface SettingsValues { 6 | defaultColorFormat: 'hex' | 'hsl' | 'rgba'; 7 | notifications: boolean; 8 | osTheme?: themes; 9 | openOnStartup: boolean; 10 | showDockIcon: boolean; 11 | sounds: boolean; 12 | userHasLoggedInBefore: boolean; 13 | userTheme: themes; 14 | } 15 | 16 | export interface SettingsStorage extends SettingsValues, StorageObject {} 17 | 18 | const Storage = StorageObject.extend(); 19 | 20 | Storage.reopenClass({ 21 | initialState(): SettingsValues { 22 | return { 23 | defaultColorFormat: 'hex', 24 | notifications: true, 25 | openOnStartup: false, 26 | showDockIcon: false, 27 | sounds: true, 28 | userHasLoggedInBefore: false, 29 | userTheme: 'dynamic', 30 | }; 31 | }, 32 | }); 33 | 34 | export default Storage; 35 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import "fonts"; 2 | @import "tailwind"; 3 | @import "variables"; 4 | @import "main"; 5 | @import "color-squares"; 6 | @import "icons"; 7 | @import "drag-sort"; 8 | @import "popover"; 9 | @import "three-dots/three-dots"; 10 | -------------------------------------------------------------------------------- /app/styles/color-squares.css: -------------------------------------------------------------------------------- 1 | .opacity-checkerboard { 2 | background-image: 3 | linear-gradient(45deg, #808080 25%, transparent 25%), 4 | linear-gradient(-45deg, #808080 25%, transparent 25%), 5 | linear-gradient(45deg, transparent 75%, #808080 75%), 6 | linear-gradient(-45deg, transparent 75%, #808080 75%); 7 | background-size: 10px 10px; 8 | background-position: 9 | 0 0, 10 | 0 5px, 11 | 5px -5px, 12 | -5px 0; 13 | } 14 | -------------------------------------------------------------------------------- /app/styles/drag-sort.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable selector-class-pattern */ 2 | .dragSortList.-isExpanded.-isDraggingOver::before, 3 | .dragSortItem.-placeholderBefore::before, 4 | .dragSortItem.-placeholderAfter::before { 5 | content: ""; 6 | display: inline-block; 7 | height: 100%; 8 | width: 5px; 9 | } 10 | 11 | .dragSortItem.-placeholderBefore::before { 12 | border-left: 5px solid var(--dragged-swatch-color); 13 | } 14 | 15 | .dragSortItem.-placeholderAfter::before { 16 | border-right: 5px solid var(--dragged-swatch-color); 17 | } 18 | 19 | /* stylelint-disable no-descending-specificity */ 20 | .dragSortList.-horizontal .dragSortItem.-placeholderBefore, 21 | .dragSortList.-horizontal.-rtl .dragSortItem.-placeholderAfter { 22 | padding: 0 0 0 5px; 23 | } 24 | 25 | .dragSortList.-horizontal .dragSortItem.-placeholderAfter, 26 | .dragSortList.-horizontal.-rtl .dragSortItem.-placeholderBefore { 27 | padding: 0 5px 0 0; 28 | } 29 | /* stylelint-enable no-descending-specificity */ 30 | 31 | .color-history-list { 32 | &.dragSortList { 33 | overflow: hidden; 34 | position: relative; 35 | } 36 | 37 | &.dragSortList.-isExpanded { 38 | padding-top: 0; 39 | } 40 | 41 | .dragSortItem.-placeholderBefore::before, 42 | .dragSortItem.-placeholderAfter::before { 43 | display: none; 44 | padding-top: 0; 45 | } 46 | 47 | .dragSortItem.-isDragged { 48 | display: block; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Inter; 3 | src: url("fonts/inter-variable.otf") format("truetype"); 4 | font-weight: 1 999; 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/icons.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable no-descending-specificity */ 2 | 3 | svg { 4 | &.drop-icon { 5 | path { 6 | fill: var(--drop-icon) !important; 7 | 8 | @apply transition-colors; 9 | } 10 | } 11 | 12 | &.icon { 13 | g, 14 | path { 15 | fill: var(--alt-color) !important; 16 | 17 | @apply transition-colors; 18 | } 19 | 20 | &:hover { 21 | g, 22 | path { 23 | fill: var(--alt-hover-color) !important; 24 | } 25 | } 26 | } 27 | 28 | &.stroke-icon { 29 | path { 30 | stroke: var(--alt-color) !important; 31 | 32 | @apply transition-colors; 33 | } 34 | 35 | &:hover { 36 | path { 37 | stroke: var(--alt-hover-color) !important; 38 | } 39 | } 40 | } 41 | 42 | &.menu-icon { 43 | path { 44 | fill: var(--menu-text-color) !important; 45 | 46 | @apply transition-colors; 47 | } 48 | } 49 | 50 | &.stroke-menu-icon { 51 | path { 52 | stroke: var(--menu-text-color) !important; 53 | 54 | @apply transition-colors; 55 | } 56 | 57 | &:hover { 58 | path { 59 | stroke: var(--menu-text-hover-color) !important; 60 | } 61 | } 62 | } 63 | } 64 | 65 | a, 66 | button { 67 | svg.icon { 68 | &.filled { 69 | path { 70 | fill: var(--alt-color) !important; 71 | 72 | @apply transition-colors; 73 | } 74 | } 75 | } 76 | 77 | &:hover:not([disabled]) { 78 | svg.icon { 79 | path { 80 | fill: var(--alt-hover-color) !important; 81 | } 82 | 83 | &.filled { 84 | path { 85 | fill: var(--alt-hover-color) !important; 86 | 87 | @apply transition-colors; 88 | } 89 | } 90 | } 91 | 92 | svg.menu-icon { 93 | path { 94 | fill: var(--menu-text-hover-color) !important; 95 | } 96 | 97 | &.filled { 98 | path { 99 | fill: var(--menu-text-hover-color) !important; 100 | 101 | @apply transition-colors; 102 | } 103 | } 104 | } 105 | 106 | svg.stroke-icon { 107 | path { 108 | stroke: var(--alt-hover-color) !important; 109 | } 110 | } 111 | } 112 | 113 | &[disabled] { 114 | cursor: not-allowed; 115 | 116 | svg.icon, 117 | svg.menu-icon { 118 | path { 119 | fill: var(--disabled-color) !important; 120 | } 121 | 122 | &.filled { 123 | path { 124 | fill: var(--disabled-color) !important; 125 | 126 | @apply transition-colors; 127 | } 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | text-size-adjust: 100%; 4 | -moz-osx-font-smoothing: grayscale; 5 | -webkit-font-smoothing: antialiased; 6 | height: 100%; 7 | font-family: Inter, sans-serif; 8 | width: 100%; 9 | } 10 | 11 | * { 12 | -ms-overflow-style: none; 13 | user-select: none; 14 | 15 | &::-webkit-scrollbar { 16 | display: none; 17 | } 18 | } 19 | 20 | a { 21 | @apply cursor-default transition-colors; 22 | 23 | &:hover { 24 | @apply cursor-default; 25 | } 26 | } 27 | 28 | button { 29 | @apply cursor-default; 30 | } 31 | 32 | textarea, 33 | select, 34 | input, 35 | button { 36 | outline: none; 37 | 38 | &:focus { 39 | outline: none; 40 | } 41 | } 42 | 43 | .color-history-container { 44 | width: calc(100% - 1.5rem); 45 | } 46 | 47 | .color-picker-popover.ember-popover { 48 | transform: translate3d(0, 34px, 0) !important; 49 | } 50 | 51 | /* stylelint-disable selector-class-pattern */ 52 | .dragSortItem { 53 | @apply leading-none; 54 | } 55 | 56 | .palette-color-squares { 57 | &.palette-locked { 58 | .dragSortItem, 59 | .dragSortItem.-isDragged { 60 | cursor: not-allowed !important; 61 | } 62 | } 63 | } 64 | /* stylelint-enable selector-class-pattern */ 65 | 66 | .selected-color { 67 | &::after { 68 | content: ""; /* Required to display content */ 69 | position: absolute; /* Sets the position absolute to the top div */ 70 | bottom: 0; 71 | left: 50%; 72 | margin-left: -5px; /* Set margin equal to border px */ 73 | width: 0; 74 | z-index: 1; 75 | height: 0; 76 | border-bottom: solid 5px #fff; /* Creates the notch */ 77 | border-left: solid 5px transparent; /* Creates triangle effect */ 78 | border-right: solid 5px transparent; /* Creates triangle effect */ 79 | } 80 | } 81 | 82 | .kuler-color-picker-container { 83 | /* stylelint-disable selector-class-pattern */ 84 | .IroWheel .IroHandle--isActive circle, 85 | .IroBox .IroHandle--isActive circle { 86 | transform: scale(1.75); 87 | } 88 | /* stylelint-enable selector-class-pattern */ 89 | } 90 | 91 | input[type="number"]::-webkit-inner-spin-button, 92 | input[type="number"]::-webkit-outer-spin-button { 93 | appearance: none; 94 | margin: 0; 95 | } 96 | 97 | button:disabled * { 98 | pointer-events: none; 99 | } 100 | 101 | .image-radio:checked + svg { 102 | @apply border-4 border-blue-600 rounded-lg; 103 | } 104 | -------------------------------------------------------------------------------- /app/styles/popover.css: -------------------------------------------------------------------------------- 1 | .ember-popover-inner > div { 2 | @apply h-full w-full; 3 | } 4 | -------------------------------------------------------------------------------- /app/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | .alert { 5 | @apply mt-3 p-3 rounded text-sm; 6 | } 7 | 8 | .alert-danger { 9 | @apply bg-red-400 text-red-800; 10 | } 11 | 12 | .alert-success { 13 | @apply bg-green-400 text-green-800; 14 | } 15 | 16 | .btn { 17 | @apply bg-btn-bg-secondary cursor-default font-medium h-10 rounded text-btn-text-secondary text-sm transition-colors; 18 | } 19 | 20 | .btn:hover { 21 | @apply bg-btn-bg-secondary-hover; 22 | } 23 | 24 | .btn-primary { 25 | @apply bg-btn-bg-primary text-btn-text-primary; 26 | } 27 | 28 | .btn-primary:hover { 29 | @apply bg-btn-bg-primary-hover; 30 | } 31 | 32 | /* TODO: add this back when we can make inputs work correctly with @tailwindcss/forms 33 | .form-checkbox { 34 | @apply border-gray-300 rounded text-checkbox focus:ring focus:ring-indigo-200 focus:ring-opacity-50; 35 | } */ 36 | 37 | .input { 38 | @apply bg-input-bg border border-input-border pb-1 pl-2 pr-2 pt-1 text-main-text text-xs; 39 | } 40 | 41 | .input-prefix { 42 | @apply absolute flex h-full items-center left-0 ml-1 text-menu-text text-center text-smallest top-0; 43 | } 44 | 45 | .tab { 46 | @apply text-center text-menu-text p-3 font-medium rounded-full text-sm hover:text-menu-text-hover; 47 | } 48 | 49 | .tab.active { 50 | @apply bg-menu text-menu-text-hover; 51 | } 52 | 53 | @tailwind utilities; 54 | -------------------------------------------------------------------------------- /app/styles/three-dots/_dot-typing.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ============================================== 3 | * Dot Typing 4 | * ============================================== 5 | */ 6 | 7 | :root { 8 | --left-pos: -9999px; 9 | --x1: calc(var(--left-pos) * -1 - var(--dot-spacing)); 10 | --x2: calc(var(--left-pos) * -1); 11 | --x3: calc(var(--left-pos) * -1 + var(--dot-spacing)); 12 | } 13 | 14 | .dot-typing { 15 | position: relative; 16 | left: var(--left-pos); 17 | 18 | width: var(--dot-width); 19 | height: var(--dot-height); 20 | border-radius: var(--dot-radius); 21 | background-color: var(--dot-bg-color); 22 | color: var(--dot-color); 23 | 24 | box-shadow: 25 | var(--x1) 0 0 0 var(--dot-before-color), 26 | var(--x2) 0 0 0 var(--dot-color), 27 | var(--x3) 0 0 0 var(--dot-after-color); 28 | animation: dot-typing 1.5s infinite linear; 29 | } 30 | 31 | @keyframes dot-typing { 32 | 0% { 33 | box-shadow: 34 | var(--x1) 0 0 0 var(--dot-before-color), 35 | var(--x2) 0 0 0 var(--dot-color), 36 | var(--x3) 0 0 0 var(--dot-after-color); 37 | } 38 | 39 | 16.667% { 40 | box-shadow: 41 | var(--x1) -10px 0 0 var(--dot-before-color), 42 | var(--x2) 0 0 0 var(--dot-color), 43 | var(--x3) 0 0 0 var(--dot-after-color); 44 | } 45 | 46 | 33.333% { 47 | box-shadow: 48 | var(--x1) 0 0 0 var(--dot-before-color), 49 | var(--x2) 0 0 0 var(--dot-color), 50 | var(--x3) 0 0 0 var(--dot-after-color); 51 | } 52 | 53 | 50% { 54 | box-shadow: 55 | var(--x1) 0 0 0 var(--dot-before-color), 56 | var(--x2) -10px 0 0 var(--dot-color), 57 | var(--x3) 0 0 0 var(--dot-after-color); 58 | } 59 | 60 | 66.667% { 61 | box-shadow: 62 | var(--x1) 0 0 0 var(--dot-before-color), 63 | var(--x2) 0 0 0 var(--dot-color), 64 | var(--x3) 0 0 0 var(--dot-after-color); 65 | } 66 | 67 | 83.333% { 68 | box-shadow: 69 | var(--x1) 0 0 0 var(--dot-before-color), 70 | var(--x2) 0 0 0 var(--dot-color), 71 | var(--x3) -10px 0 0 var(--dot-after-color); 72 | } 73 | 74 | 100% { 75 | box-shadow: 76 | var(--x1) 0 0 0 var(--dot-before-color), 77 | var(--x2) 0 0 0 var(--dot-color), 78 | var(--x3) 0 0 0 var(--dot-after-color); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/styles/three-dots/_variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dot-width: 8px; 3 | --dot-height: 8px; 4 | --dot-radius: calc(var(--dot-width) / 2); 5 | --dot-spacing: calc(var(--dot-width) + var(--dot-width) / 2); 6 | } 7 | 8 | body.light { 9 | --dot-color: #fff; 10 | --dot-bg-color: #fff; 11 | --dot-before-color: #fff; 12 | --dot-after-color: #fff; 13 | } 14 | 15 | body.dark { 16 | --dot-color: #0e0f1f; 17 | --dot-bg-color: #0e0f1f; 18 | --dot-before-color: #0e0f1f; 19 | --dot-after-color: #0e0f1f; 20 | } 21 | -------------------------------------------------------------------------------- /app/styles/three-dots/three-dots.css: -------------------------------------------------------------------------------- 1 | /* ------------------------------ 2 | Three-Dots 3 | ------------------------------ */ 4 | 5 | @import "_variables.css"; 6 | @import "_dot-typing.css"; 7 | -------------------------------------------------------------------------------- /app/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --dragged-swatch-color: #677087; 3 | } 4 | 5 | body.light { 6 | --alt-color: #b6b6ca; 7 | --alt-hover-color: #66667c; 8 | --btn-bg-primary: #1f1b3b; 9 | --btn-bg-primary-hover: #383069; 10 | --btn-bg-secondary: #cfd3df; 11 | --btn-bg-secondary-hover: #e4e8f2; 12 | --btn-text-primary: #fff; 13 | --btn-text-secondary: #505768; 14 | --checkbox: #1f1b3b; 15 | --disabled-color: #a4acc6; 16 | --drop-icon: #0e1c2a; 17 | --heading-color: #697088; 18 | --input-bg: #f1f2f5; 19 | --input-border: #e7e7ed; 20 | --main-color: #e7e7ed; 21 | --menu-color: #fff; 22 | --menu-text-color: #697088; 23 | --menu-text-hover-color: #363a48; 24 | --main-text: #363a48; 25 | --sub-text: #9ca2b1; 26 | } 27 | 28 | body.dark { 29 | --alt-color: #7a7ca5; 30 | --alt-hover-color: #a4a7d9; 31 | --btn-bg-primary: #fff; 32 | --btn-bg-primary-hover: #f2f4fb; 33 | --btn-bg-secondary: #383a55; 34 | --btn-bg-secondary-hover: #4b4d72; 35 | --btn-text-primary: #0e0f1f; 36 | --btn-text-secondary: #d8dff4; 37 | --checkbox: #383a55; 38 | --disabled-color: #5e6078; 39 | --drop-icon: #fff; 40 | --heading-color: #a9acd3; 41 | --input-bg: #0e0f1f; 42 | --input-border: #000; 43 | --main-color: #0e0f1f; 44 | --menu-color: #222336; 45 | --menu-text-color: #a9acd3; 46 | --menu-text-hover-color: #fff; 47 | --main-text: #fff; 48 | --sub-text: #9ca2b1; 49 | } 50 | -------------------------------------------------------------------------------- /app/templates/colors.hbs: -------------------------------------------------------------------------------- 1 | 5 | {{svg-jar "chevron-left" class="stroke-icon mr-2" height="15" width="15"}} 6 | Palettes 7 | 8 | 9 |
10 | {{if @model.isColorHistory "Color History" @model.name}} 11 |
12 | 13 | -------------------------------------------------------------------------------- /app/templates/contrast.hbs: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /app/templates/kuler.hbs: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /app/templates/settings.hbs: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 |
15 | {{outlet}} 16 |
-------------------------------------------------------------------------------- /app/templates/settings/cloud.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{svg-jar "cloud"}} 3 | 4 |
5 | Cloud Sync 6 |
7 | 8 |

9 | Use cloud sync to keep all your palettes and colors up to date across all 10 | your devices. 11 |

12 |
13 | 14 | {{outlet}} -------------------------------------------------------------------------------- /app/templates/settings/cloud/forgot-password.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/settings/cloud/login.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/settings/cloud/profile.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{#if @model.email}} 4 |

5 | Signed in as... 6 |

7 | 8 |
12 | {{@model.email}} 13 |
14 | {{/if}} 15 |
16 | 17 |
18 | 24 | Sign Out 25 | 26 |
27 |
-------------------------------------------------------------------------------- /app/templates/settings/cloud/register/confirm.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/settings/cloud/register/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/settings/data.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/settings/index.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/welcome.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} -------------------------------------------------------------------------------- /app/templates/welcome/auto-start.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{svg-jar "swach" class=" h-36 mb-8 w-36"}} 5 |
6 | 7 |

8 | Start Swach Automatically 9 |

10 | 11 |
12 |

13 | Would you like Swach to start every time you start your computer? 14 |

15 | 16 |
17 | 26 |
27 |
28 | 29 |
30 | 34 | Previous 35 | 36 | 37 | 42 | Next 43 | 44 |
45 |
46 |
-------------------------------------------------------------------------------- /app/templates/welcome/cloud-sync.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{svg-jar "cloud" class="h-36 mb-8 w-36"}} 5 |
6 | 7 |

8 | Sync data to the cloud 9 |

10 | 11 |
12 |

13 | Swach now supports creating accounts to sync your data to the cloud. 14 | This is useful if you use multiple devices or want to make sure your 15 | data is backed up. Once you log in for the first time, you will be 16 | required 17 | to log in to use Swach after that. 18 |

19 | 20 |
21 | Would you like to create an account now? 22 |
23 |
24 | 25 |
26 | 30 | Previous 31 | 32 | 33 | 37 | Yes, please! (recommended) 38 | 39 | 40 | 44 | No account for now, thanks. 45 | 46 |
47 |
48 |
-------------------------------------------------------------------------------- /app/templates/welcome/dock-icon.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{svg-jar "show-dock-icon" class="h-auto mb-8 w-full"}} 5 |
6 | 7 |

8 | Show Dock Icon 9 |

10 | 11 |
12 |

13 | Swach lives in your menubar and hides the dock icon by default. This 14 | setting allows you to show the dock icon, and can be useful if the 15 | menubar icon is not showing up. 16 |

17 | 18 |
19 | 28 |
29 |
30 | 31 |
32 | 36 | Previous 37 | 38 | 39 | 43 | Next 44 | 45 |
46 |
47 |
-------------------------------------------------------------------------------- /app/templates/welcome/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{svg-jar "swach" class="h-20 mb-8 w-20"}} 5 |
6 | 7 |

8 | Welcome to Swach 1.0.0! 9 |

10 | 11 |

12 | Please take a moment to configure Swach to fit your preferred work style. 13 | You may have configured these settings before, but some things have 14 | changed in the 1.x release. 15 |

16 | 17 |
18 | 23 | Next 24 | 25 |
26 |
27 |
-------------------------------------------------------------------------------- /app/transitions.js: -------------------------------------------------------------------------------- 1 | import { easeInAndOut } from 'ember-animated/easings/cosine'; 2 | import { toLeft, toRight } from 'ember-animated/transitions/move-over'; 3 | 4 | export const transitionOptions = { duration: 250, easing: easeInAndOut }; 5 | 6 | export const transitions = [ 7 | { 8 | from: 'welcome.index', 9 | to: 'welcome.auto-start', 10 | use: toLeft, 11 | reverse: toRight, 12 | }, 13 | { 14 | from: 'welcome.auto-start', 15 | to: 'welcome.dock-icon', 16 | use: toLeft, 17 | reverse: toRight, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /app/utils/get-db-open-request.ts: -------------------------------------------------------------------------------- 1 | import ENV from 'swach/config/environment'; 2 | 3 | const { SCHEMA_VERSION } = ENV; 4 | 5 | export function getDBOpenRequest(): IDBOpenDBRequest { 6 | return window.indexedDB.open('orbit', SCHEMA_VERSION); 7 | } 8 | -------------------------------------------------------------------------------- /app/utils/remove-from-to.ts: -------------------------------------------------------------------------------- 1 | export default function removeFromTo( 2 | array: unknown[], 3 | from: number, 4 | to: number, 5 | ): number { 6 | array.splice( 7 | from, 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 9 | !to || 10 | // @ts-expect-error: We need to refactor this function 11 | 1 + 12 | to - 13 | from + 14 | // @ts-expect-error: We need to refactor this function 15 | (!((to < 0) ^ (from >= 0)) && (to < 0 || -1) * array.length), 16 | ); 17 | 18 | return array.length; 19 | } 20 | -------------------------------------------------------------------------------- /app/utils/view-transitions.ts: -------------------------------------------------------------------------------- 1 | export default function viewTransitions() { 2 | if (!document.startViewTransition) { 3 | return; 4 | } 5 | 6 | return new Promise((resolve) => { 7 | // eslint-disable-next-line @typescript-eslint/require-await 8 | document.startViewTransition(async () => { 9 | resolve(); 10 | }); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /config/content-security-policy.js: -------------------------------------------------------------------------------- 1 | // config/content-security-policy.js 2 | 3 | module.exports = function (environment) { 4 | return { 5 | delivery: ['meta'], 6 | enabled: environment !== 'test', 7 | failTests: true, 8 | policy: { 9 | 'default-src': ["'none'"], 10 | 'script-src': ['http://localhost:7020', "'self'", "'unsafe-inline'"], 11 | 'font-src': ["'self'"], 12 | 'frame-src': ["'self'"], 13 | 'connect-src': [ 14 | 'https://cognito-idp.us-east-2.amazonaws.com/', 15 | 'https://cognito-identity.us-east-2.amazonaws.com/', 16 | 'https://jpuj8ukmx8.execute-api.us-east-2.amazonaws.com/dev/', 17 | 'https://n3tygwauml.execute-api.us-east-2.amazonaws.com/prod/', 18 | 'https://sentry.io/', 19 | 'http://localhost:3000', 20 | "'self'", 21 | ], 22 | 'img-src': ['data:', "'self'"], 23 | 'style-src': ["'self'", "'unsafe-inline'"], 24 | 'media-src': ["'self'"], 25 | }, 26 | reportOnly: true, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "6.4.0", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--pnpm", 15 | "--ci-provider=github", 16 | "--typescript" 17 | ] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true, 6 | "no-implicit-route-model": true 7 | } 8 | -------------------------------------------------------------------------------- /config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | browsers: ['electron >= 27.0.0'], 5 | }; 6 | -------------------------------------------------------------------------------- /electron-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | .DS_Store 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Electron-Builder 89 | dist/ 90 | 91 | # Electron-Forge 92 | out/ 93 | 94 | # Ember build 95 | ember-dist/ 96 | ember-test/ 97 | 98 | # Sentry 99 | .electron-symbols/ 100 | sentry.properties 101 | -------------------------------------------------------------------------------- /electron-app/example.sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=ship-shape-consulting-llc 3 | defaults.project=swach 4 | auth.token= 5 | cli.executable=../../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli 6 | -------------------------------------------------------------------------------- /electron-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swach", 3 | "productName": "swach", 4 | "version": "1.2.15", 5 | "description": "A robust color management tool for the modern age.", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "start": "electron-forge start", 9 | "package": "electron-forge package", 10 | "make": "electron-forge make", 11 | "publish": "electron-forge publish", 12 | "lint": "echo \"No linting configured\"" 13 | }, 14 | "keywords": [], 15 | "author": { 16 | "name": "Robert Wagner", 17 | "email": "rwwagner90@gmail.com", 18 | "url": "https://github.com/rwwagner90" 19 | }, 20 | "license": "MIT", 21 | "config": { 22 | "forge": "forge.config.js" 23 | }, 24 | "dependencies": { 25 | "@sentry/electron": "^4.24.0", 26 | "electron-dl": "^3.5.2", 27 | "electron-is-dev": "^2.0.0", 28 | "electron-squirrel-startup": "^1.0.1", 29 | "electron-store": "^8.2.0", 30 | "indexeddb-export-import": "^2.1.5", 31 | "menubar": "^9.5.1", 32 | "throttle-debounce": "^5.0.2" 33 | }, 34 | "devDependencies": { 35 | "@electron-forge/cli": "^7.8.1", 36 | "@electron-forge/core": "^7.8.1", 37 | "@electron-forge/maker-deb": "^7.8.1", 38 | "@electron-forge/maker-dmg": "^7.8.1", 39 | "@electron-forge/maker-snap": "^7.8.1", 40 | "@electron-forge/maker-squirrel": "^7.8.1", 41 | "@electron-forge/maker-zip": "^7.8.1", 42 | "@electron-forge/publisher-snapcraft": "^7.8.1", 43 | "@sentry/cli": "^2.39.1", 44 | "electron": "^33.2.1", 45 | "electron-debug": "^3.2.0", 46 | "electron-download": "^4.1.1" 47 | }, 48 | "packageManager": "pnpm@10.11.0", 49 | "engines": { 50 | "node": ">= 20", 51 | "pnpm": "^10.11.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /electron-app/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/.gitkeep -------------------------------------------------------------------------------- /electron-app/resources/dmg.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/dmg.icns -------------------------------------------------------------------------------- /electron-app/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/icon.icns -------------------------------------------------------------------------------- /electron-app/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/icon.ico -------------------------------------------------------------------------------- /electron-app/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/icon.png -------------------------------------------------------------------------------- /electron-app/resources/installBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/installBackground.png -------------------------------------------------------------------------------- /electron-app/resources/installBackground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/installBackground@2x.png -------------------------------------------------------------------------------- /electron-app/resources/menubar-icons/iconTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/menubar-icons/iconTemplate.png -------------------------------------------------------------------------------- /electron-app/resources/menubar-icons/iconTemplate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/menubar-icons/iconTemplate@2x.png -------------------------------------------------------------------------------- /electron-app/resources/png/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/1024x1024.png -------------------------------------------------------------------------------- /electron-app/resources/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/128x128.png -------------------------------------------------------------------------------- /electron-app/resources/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/16x16.png -------------------------------------------------------------------------------- /electron-app/resources/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/24x24.png -------------------------------------------------------------------------------- /electron-app/resources/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/256x256.png -------------------------------------------------------------------------------- /electron-app/resources/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/32x32.png -------------------------------------------------------------------------------- /electron-app/resources/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/48x48.png -------------------------------------------------------------------------------- /electron-app/resources/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/512x512.png -------------------------------------------------------------------------------- /electron-app/resources/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/64x64.png -------------------------------------------------------------------------------- /electron-app/resources/png/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/electron-app/resources/png/96x96.png -------------------------------------------------------------------------------- /electron-app/src/auto-update.js: -------------------------------------------------------------------------------- 1 | const { autoUpdater, dialog } = require('electron'); 2 | 3 | const setupUpdateServer = (app) => { 4 | const server = 'https://download.swach.io'; 5 | const feed = `${server}/update/${process.platform}/${app.getVersion()}`; 6 | 7 | autoUpdater.setFeedURL(feed); 8 | 9 | // Checks for updates every 30 minutes 10 | const checkForUpdatesInterval = setInterval( 11 | () => { 12 | autoUpdater.checkForUpdates(); 13 | }, 14 | 30 * 60 * 1000, 15 | ); 16 | 17 | autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { 18 | clearInterval(checkForUpdatesInterval); 19 | autoUpdater.removeAllListeners('update-not-available'); 20 | const dialogOpts = { 21 | type: 'question', 22 | buttons: ['Restart', 'Later'], 23 | title: 'Application Update', 24 | message: process.platform === 'win32' ? releaseNotes : releaseName, 25 | detail: 26 | 'A new version has been downloaded. Restart the application to apply the updates.', 27 | }; 28 | 29 | dialog.showMessageBox(dialogOpts).then((returnValue) => { 30 | if (returnValue.response === 0) autoUpdater.quitAndInstall(); 31 | }); 32 | }); 33 | 34 | autoUpdater.on('error', (message) => { 35 | autoUpdater.removeAllListeners('update-not-available'); 36 | console.error('There was a problem updating the application'); 37 | console.error(message); 38 | }); 39 | 40 | return autoUpdater; 41 | }; 42 | 43 | module.exports = { 44 | setupUpdateServer, 45 | }; 46 | -------------------------------------------------------------------------------- /electron-app/src/browsers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (dirname) => ({ 4 | settings: require('./window')(dirname, 'settings', 'Settings'), 5 | }); 6 | -------------------------------------------------------------------------------- /electron-app/src/browsers/window.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BrowserWindow } = require('electron'); 4 | 5 | module.exports = (dirname, route, title) => { 6 | let win; 7 | 8 | /** 9 | * [init] 10 | * @param {boolean} force [force launching new window] 11 | * @return {void} [new Colorpicker] 12 | */ 13 | let init = () => { 14 | if (win === null || win === undefined) createWindow(); 15 | else win.show(); 16 | 17 | // win.openDevTools(); 18 | }; 19 | 20 | /** 21 | * [createWindow - create new Window] 22 | * @param {int} width [width of the window] 23 | * @param {int} height [height of the window] 24 | * @return {void} 25 | */ 26 | let createWindow = () => { 27 | let options = { 28 | width: 700, 29 | height: 500, 30 | minWidth: 460, 31 | minHeight: 340, 32 | fullscreenable: false, 33 | titleBarStyle: 'hidden', 34 | title, 35 | webPreferences: { 36 | contextIsolation: false, 37 | nodeIntegration: true, 38 | }, 39 | }; 40 | 41 | win = new BrowserWindow(options); 42 | const windowRoute = `serve://dist#/${route}`; 43 | win.loadURL(windowRoute); 44 | 45 | win.on('closed', () => { 46 | win = undefined; 47 | }); 48 | 49 | win.on('page-title-updated', function (e) { 50 | e.preventDefault(); 51 | }); 52 | }; 53 | 54 | let getWindow = () => win; 55 | 56 | return { 57 | init: init, 58 | getWindow: getWindow, 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /electron-app/src/color-picker.js: -------------------------------------------------------------------------------- 1 | async function launchPicker(mb, type = 'global') { 2 | mb.hideWindow(); 3 | 4 | const color = await mb.window.webContents.executeJavaScript( 5 | ` 6 | async function openEyeDropper() { 7 | const eyeDropper = new EyeDropper(); 8 | 9 | let color = ''; 10 | 11 | try { 12 | const result = await eyeDropper.open(); 13 | color = result.sRGBHex; 14 | } catch (error) { 15 | console.warn(\`[ERROR] launchPicker EyeDropper\`, error); 16 | } 17 | 18 | return color; 19 | } 20 | 21 | openEyeDropper();`, 22 | true, 23 | ); 24 | 25 | if (color) { 26 | if (type === 'global') { 27 | mb.window.webContents.send('changeColor', color); 28 | } 29 | if (type === 'contrastBg') { 30 | mb.window.webContents.send('pickContrastBgColor', color); 31 | } 32 | if (type === 'contrastFg') { 33 | mb.window.webContents.send('pickContrastFgColor', color); 34 | } 35 | } 36 | 37 | mb.showWindow(); 38 | } 39 | 40 | module.exports = { 41 | launchPicker, 42 | }; 43 | -------------------------------------------------------------------------------- /electron-app/src/dialogs.js: -------------------------------------------------------------------------------- 1 | const { app, dialog } = require('electron'); 2 | 3 | function noUpdatesAvailableDialog() { 4 | const dialogOpts = { 5 | type: 'info', 6 | title: 'Already up to date', 7 | message: 'Already up to date', 8 | detail: `Swach ${app.getVersion()} is the latest version available.`, 9 | }; 10 | 11 | return dialog.showMessageBox(dialogOpts); 12 | } 13 | 14 | function restartDialog() { 15 | const dialogOpts = { 16 | type: 'question', 17 | buttons: ['Restart', 'Later'], 18 | title: 'Restart Required', 19 | message: 'Restart now?', 20 | detail: 'A restart is required to apply this setting. Restart now?', 21 | defaultId: 0, 22 | }; 23 | 24 | return dialog.showMessageBox(dialogOpts).then((returnValue) => { 25 | if (returnValue.response === 0) { 26 | app.relaunch(); 27 | app.exit(); 28 | } 29 | }); 30 | } 31 | 32 | module.exports = { 33 | noUpdatesAvailableDialog, 34 | restartDialog, 35 | }; 36 | -------------------------------------------------------------------------------- /electron-app/src/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.cs.disable-executable-page-protection 12 | 13 | com.apple.security.automation.apple-events 14 | 15 | 16 | -------------------------------------------------------------------------------- /electron-app/src/handle-file-urls.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { fileURLToPath, pathToFileURL } = require('url'); 4 | const { promisify } = require('util'); 5 | 6 | const access = promisify(fs.access); 7 | 8 | // 9 | // Patch asset loading -- Ember apps use absolute paths to reference their 10 | // assets, e.g. ``. When the current URL is a `file:` 11 | // URL, that ends up resolving to the absolute filesystem path `/images/foo.jpg` 12 | // rather than being relative to the root of the Ember app. So, we intercept 13 | // `file:` URL request and look to see if they point to an asset when 14 | // interpreted as being relative to the root of the Ember app. If so, we return 15 | // that path, and if not we leave them as-is, as their absolute path. 16 | // 17 | async function getAssetPath(emberAppDir, url) { 18 | let urlPath = fileURLToPath(url); 19 | // Get the root of the path -- should be '/' on MacOS or something like 20 | // 'C:\' on Windows 21 | let { root } = path.parse(urlPath); 22 | // Get the relative path from the root to the full path 23 | let relPath = path.relative(root, urlPath); 24 | // Join the relative path with the Ember app directory 25 | let appPath = path.join(emberAppDir, relPath); 26 | try { 27 | await access(appPath); 28 | return appPath; 29 | } catch { 30 | return urlPath; 31 | } 32 | } 33 | 34 | module.exports = function handleFileURLs(emberAppDir) { 35 | const { protocol, net } = require('electron'); 36 | 37 | if (protocol.handle) { 38 | // Electron >= 25 39 | protocol.handle('file', async ({ url }) => { 40 | let path = await getAssetPath(emberAppDir, url); 41 | return net.fetch(pathToFileURL(path), { 42 | bypassCustomProtocolHandlers: true, 43 | }); 44 | }); 45 | } else { 46 | // Electron < 25 47 | protocol.interceptFileProtocol('file', async ({ url }, callback) => { 48 | callback(await getAssetPath(emberAppDir, url)); 49 | }); 50 | } 51 | }; 52 | 53 | module.exports.getAssetPath = getAssetPath; 54 | -------------------------------------------------------------------------------- /electron-app/src/ipc-events.js: -------------------------------------------------------------------------------- 1 | const { 2 | app, 3 | clipboard, 4 | dialog, 5 | ipcMain, 6 | nativeTheme, 7 | shell, 8 | } = require('electron'); 9 | const { download } = require('electron-dl'); 10 | const fs = require('fs'); 11 | 12 | const { launchPicker } = require('./color-picker'); 13 | const { restartDialog } = require('./dialogs'); 14 | 15 | function setupEventHandlers(mb, store) { 16 | ipcMain.on('copyColorToClipboard', (channel, color) => { 17 | clipboard.writeText(color); 18 | }); 19 | 20 | ipcMain.on('exitApp', () => mb.app.quit()); 21 | 22 | ipcMain.on('exportData', async (channel, jsonString) => { 23 | const downloadPath = `${mb.app.getPath('temp')}/swach-data.json`; 24 | fs.writeFileSync(downloadPath, jsonString); 25 | await download(mb.window, `file://${downloadPath}`); 26 | fs.unlink(downloadPath, (err) => { 27 | if (err) throw err; 28 | console.log(`${downloadPath} was deleted`); 29 | }); 30 | }); 31 | 32 | ipcMain.handle('getAppVersion', async () => { 33 | return app.getVersion(); 34 | }); 35 | 36 | ipcMain.handle('getBackupData', async () => { 37 | const backupPath = `${mb.app.getPath('temp')}/backup-swach-data.json`; 38 | return fs.readFileSync(backupPath, { encoding: 'utf8' }); 39 | }); 40 | 41 | ipcMain.handle('getPlatform', () => { 42 | return process.platform; 43 | }); 44 | 45 | ipcMain.handle('getStoreValue', (event, key) => { 46 | return store.get(key); 47 | }); 48 | 49 | ipcMain.handle('getShouldUseDarkColors', () => { 50 | return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; 51 | }); 52 | 53 | ipcMain.handle('importData', async () => { 54 | const { canceled, filePaths } = await dialog.showOpenDialog({ 55 | properties: ['openFile'], 56 | }); 57 | 58 | if (!canceled && filePaths.length) { 59 | return fs.readFileSync(filePaths[0], { encoding: 'utf8' }); 60 | } 61 | }); 62 | 63 | ipcMain.on('launchContrastBgPicker', async () => { 64 | await launchPicker(mb, 'contrastBg'); 65 | }); 66 | 67 | ipcMain.on('launchContrastFgPicker', async () => { 68 | await launchPicker(mb, 'contrastFg'); 69 | }); 70 | 71 | ipcMain.on('launchPicker', async () => { 72 | await launchPicker(mb); 73 | }); 74 | 75 | ipcMain.handle('open-external', async (_event, url) => { 76 | await shell.openExternal(url); 77 | }); 78 | 79 | ipcMain.on('setShowDockIcon', async (channel, showDockIcon) => { 80 | store.set('showDockIcon', showDockIcon); 81 | await restartDialog(); 82 | }); 83 | } 84 | 85 | module.exports = { 86 | setupEventHandlers, 87 | }; 88 | -------------------------------------------------------------------------------- /electron-app/src/preload.js: -------------------------------------------------------------------------------- 1 | const Sentry = require('@sentry/electron'); 2 | 3 | Sentry.init({ 4 | appName: 'swach', 5 | dsn: 'https://6974b46329f24dc1b9fca4507c65e942@sentry.io/3956140', 6 | release: `v${require('../package').version}`, 7 | }); 8 | -------------------------------------------------------------------------------- /electron-app/tests/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | setupTestem, 3 | openTestWindow, 4 | } = require('ember-electron/lib/test-support'); 5 | 6 | const { app, ipcMain, nativeTheme } = require('electron'); 7 | const Store = require('electron-store'); 8 | const path = require('path'); 9 | 10 | const store = new Store({ 11 | defaults: { 12 | firstRunV1: true, 13 | needsMigration: true, 14 | showDockIcon: false, 15 | }, 16 | }); 17 | 18 | const handleFileUrls = require('../src/handle-file-urls'); 19 | 20 | const emberAppDir = path.resolve(__dirname, '..', 'ember-test'); 21 | 22 | ipcMain.handle('getAppVersion', async () => { 23 | return app.getVersion(); 24 | }); 25 | 26 | ipcMain.handle('getPlatform', () => { 27 | return process.platform; 28 | }); 29 | 30 | ipcMain.handle('getStoreValue', (event, key) => { 31 | return store.get(key); 32 | }); 33 | 34 | ipcMain.handle('getShouldUseDarkColors', () => { 35 | return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; 36 | }); 37 | 38 | app.on('ready', async function onReady() { 39 | await handleFileUrls(emberAppDir); 40 | setupTestem(); 41 | openTestWindow(emberAppDir); 42 | }); 43 | 44 | app.on('window-all-closed', function onWindowAllClosed() { 45 | if (process.platform !== 'darwin') { 46 | app.quit(); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | const app = new EmberApp(defaults, { 7 | autoImport: { 8 | forbidEval: true, 9 | }, 10 | babel: { 11 | plugins: ['@babel/plugin-proposal-object-rest-spread'], 12 | }, 13 | 'ember-cli-babel': { 14 | enableTypeScriptTransform: true, 15 | }, 16 | postcssOptions: { 17 | compile: { 18 | enabled: true, 19 | plugins: [ 20 | require('postcss-import')(), 21 | require('tailwindcss')('./tailwind.config.js'), 22 | ], 23 | }, 24 | }, 25 | sourcemaps: { 26 | enabled: true, 27 | }, 28 | }); 29 | 30 | if (process.platform !== 'win32') { 31 | const { Webpack } = require('@embroider/webpack'); 32 | 33 | //const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 34 | return require('@embroider/compat').compatBuild(app, Webpack, { 35 | staticAddonTestSupportTrees: true, 36 | staticAddonTrees: true, 37 | staticHelpers: true, 38 | staticComponents: true, 39 | packagerOptions: { 40 | webpackConfig: { 41 | devtool: false, 42 | resolve: { 43 | fallback: { 44 | crypto: require.resolve('crypto-browserify'), 45 | stream: require.resolve('stream-browserify'), 46 | }, 47 | }, 48 | }, 49 | }, 50 | }); 51 | } else { 52 | return app.toTree(); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | }, 5 | "exclude": [ 6 | "node_modules", 7 | "dist", 8 | "electron-app/ember-dist", 9 | "electron-app/node_modules", 10 | "electron-app/out" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - electron-app 3 | onlyBuiltDependencies: 4 | - '@parcel/watcher' 5 | - '@sentry/cli' 6 | - core-js 7 | - electron 8 | - electron-winstaller 9 | - fs-xattr 10 | - fsevents 11 | - macos-alias 12 | -------------------------------------------------------------------------------- /public/assets/fonts/inter-variable.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/public/assets/fonts/inter-variable.otf -------------------------------------------------------------------------------- /public/assets/sounds/marimba_chromatic.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/public/assets/sounds/marimba_chromatic.wav -------------------------------------------------------------------------------- /public/assets/sounds/pluck_short.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shipshapecode/swach/61d50c65f5ec421dcf839c3780f6a5f66a16fbce/public/assets/sounds/pluck_short.wav -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/svgs/alert-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/appearance/dark-theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/svgs/appearance/dynamic-theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/svgs/appearance/light-theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/svgs/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/chevron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/clear-history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/svgs/color-harmonies.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/contrast.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/drop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/duplicate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/edit-color.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/filled-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/more-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/outline-heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/palettes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/plus-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/rename.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/save.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svgs/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/slash-heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/unlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/svgs/x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const path = require('node:path'); 3 | const appEntry = path.join(__dirname, 'app'); 4 | const relevantFilesGlob = '**/*.{html,js,ts,hbs,gjs,gts}'; 5 | module.exports = { 6 | content: [path.join(appEntry, relevantFilesGlob)], 7 | theme: { 8 | extend: { 9 | colors: { 10 | alt: 'var(--alt-color)', 11 | 'alt-hover': 'var(--alt-hover-color)', 12 | 'btn-bg-primary': 'var(--btn-bg-primary)', 13 | 'btn-bg-primary-hover': 'var(--btn-bg-primary-hover)', 14 | 'btn-bg-secondary': 'var(--btn-bg-secondary)', 15 | 'btn-bg-secondary-hover': 'var(--btn-bg-secondary-hover)', 16 | 'btn-text-primary': 'var(--btn-text-primary)', 17 | 'btn-text-secondary': 'var(--btn-text-secondary)', 18 | checkbox: 'var(--checkbox)', 19 | heading: 'var(--heading-color)', 20 | 'input-bg': 'var(--input-bg)', 21 | 'input-border': 'var(--input-border)', 22 | main: 'var(--main-color)', 23 | menu: 'var(--menu-color)', 24 | 'menu-text': 'var(--menu-text-color)', 25 | 'menu-text-hover': 'var(--menu-text-hover-color)', 26 | 'main-text': 'var(--main-text)', 27 | 'sub-text': 'var(--sub-text)', 28 | }, 29 | fontSize: { 30 | smallest: '0.5rem', 31 | xxs: '0.65rem', 32 | }, 33 | width: { 34 | 36: '9rem', 35 | }, 36 | }, 37 | fill: (theme) => ({ 38 | alt: theme('colors.alt'), 39 | 'alt-hover': theme('colors.alt-hover'), 40 | main: theme('colors.main'), 41 | }), 42 | stroke: (theme) => ({ 43 | alt: theme('colors.alt'), 44 | 'alt-hover': theme('colors.alt-hover'), 45 | main: theme('colors.main'), 46 | }), 47 | }, 48 | plugins: [ 49 | require('@tailwindcss/forms')({ 50 | strategy: 'class', 51 | }), 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /testem-electron.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launchers: { 5 | Electron: require('ember-electron/lib/test-runner'), 6 | }, 7 | launch_in_ci: ['Electron'], 8 | launch_in_dev: ['Electron'], 9 | browser_start_timeout: 120, 10 | browser_args: { 11 | Electron: { 12 | // Note: Some these Chrome options may not be supported in Electron 13 | // See https://electronjs.org/docs/api/chrome-command-line-switches 14 | ci: [ 15 | // --no-sandbox is needed when running Chrome inside a container 16 | process.env.CI ? '--no-sandbox' : null, 17 | '--headless', 18 | '--disable-dev-shm-usage', 19 | '--disable-software-rasterizer', 20 | '--mute-audio', 21 | '--remote-debugging-port=0', 22 | '--window-size=1440,900', 23 | ].filter(Boolean), 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/acceptance/contrast-test.js: -------------------------------------------------------------------------------- 1 | import { 2 | currentURL, 3 | fillIn, 4 | triggerKeyEvent, 5 | visit, 6 | } from '@ember/test-helpers'; 7 | import { module, test } from 'qunit'; 8 | 9 | import { resetStorage, waitForAll } from 'swach/tests/helpers'; 10 | import { setupApplicationTest } from 'swach/tests/helpers/index'; 11 | 12 | module('Acceptance | contrast', function (hooks) { 13 | setupApplicationTest(hooks); 14 | resetStorage(hooks, { seed: { source: 'backup', scenario: 'basic' } }); 15 | 16 | hooks.beforeEach(async function () { 17 | await visit('/contrast'); 18 | }); 19 | 20 | test('visiting /contrast', async function (assert) { 21 | assert.strictEqual(currentURL(), '/contrast'); 22 | }); 23 | 24 | test('has default value on open', function (assert) { 25 | assert.dom('[data-test-wcag-score]').hasText('21.00'); 26 | assert.dom('[data-test-wcag-string]').hasText('AAA'); 27 | }); 28 | 29 | test('updates score when failing background value added', async function (assert) { 30 | await waitForAll(); 31 | 32 | await fillIn('[data-test-bg-input]', '#504F4F'); 33 | await triggerKeyEvent('[data-test-bg-input]', 'keypress', 13); 34 | 35 | await waitForAll(); 36 | 37 | assert.dom('[data-test-wcag-score]').hasText('2.57'); 38 | assert.dom('[data-test-wcag-string]').hasText('Fail'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/acceptance/from-scratch-test.js: -------------------------------------------------------------------------------- 1 | import { click, fillIn, triggerEvent, visit } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | 4 | import { resetStorage, waitForAll } from 'swach/tests/helpers'; 5 | import { setupApplicationTest } from 'swach/tests/helpers/index'; 6 | 7 | module('Acceptance | from scratch', function (hooks) { 8 | setupApplicationTest(hooks); 9 | resetStorage(hooks); 10 | 11 | test('add a color', async function (assert) { 12 | await visit('/palettes'); 13 | 14 | assert 15 | .dom('[data-test-color-history] [data-test-color-history-square]') 16 | .doesNotExist(); 17 | 18 | await click('[data-test-toggle-color-picker]'); 19 | 20 | await waitForAll(); 21 | 22 | await fillIn('[data-test-color-picker-hex]', '#ffffff0e'); 23 | await triggerEvent('[data-test-color-picker-hex]', 'complete'); 24 | 25 | await waitForAll(); 26 | 27 | await click('[data-test-color-picker-save]'); 28 | 29 | await waitForAll(); 30 | 31 | assert 32 | .dom('[data-test-color-history] [data-test-color-history-square]') 33 | .exists({ count: 1 }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/acceptance/index-test.js: -------------------------------------------------------------------------------- 1 | import { currentURL, visit } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | 4 | import { resetStorage } from 'swach/tests/helpers'; 5 | import { setupApplicationTest } from 'swach/tests/helpers/index'; 6 | 7 | module('Acceptance | index', function (hooks) { 8 | setupApplicationTest(hooks); 9 | resetStorage(hooks, { seed: { source: 'backup', scenario: 'basic' } }); 10 | 11 | test('visiting /index', async function (assert) { 12 | await visit('/'); 13 | 14 | assert.strictEqual(currentURL(), '/palettes'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/acceptance/settings-test.js: -------------------------------------------------------------------------------- 1 | import { click, currentURL, visit } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | 4 | import { resetStorage } from 'swach/tests/helpers'; 5 | import { setupApplicationTest } from 'swach/tests/helpers/index'; 6 | 7 | module('Acceptance | settings', function (hooks) { 8 | setupApplicationTest(hooks); 9 | resetStorage(hooks, { seed: { source: 'backup', scenario: 'basic' } }); 10 | 11 | hooks.beforeEach(async function () { 12 | await visit('/settings'); 13 | }); 14 | 15 | test('visiting /settings', function (assert) { 16 | assert.strictEqual(currentURL(), '/settings'); 17 | }); 18 | 19 | test('settings menu is shown', function (assert) { 20 | assert.dom('[data-test-settings-menu]').exists(); 21 | }); 22 | 23 | test('sounds is checked by default', function (assert) { 24 | assert.dom('[data-test-settings-sounds]').isChecked(); 25 | }); 26 | 27 | test('theme setting updates when selected', async function (assert) { 28 | await click('[data-test-settings-select-theme="light"]'); 29 | 30 | const theme = JSON.parse( 31 | localStorage.getItem('storage:settings'), 32 | ).userTheme; 33 | 34 | assert.strictEqual(theme, 'light'); 35 | }); 36 | 37 | // Ember specific tests 38 | if (typeof requireNode === 'undefined') { 39 | test('has five inputs', function (assert) { 40 | assert.dom('[data-test-settings-menu] input').exists({ count: 5 }); 41 | }); 42 | } 43 | 44 | // Electron specific tests 45 | if (typeof requireNode !== 'undefined') { 46 | // TODO: these are different for Mac/Windows vs Linux, so we need specific platform tests 47 | // test('has seven inputs', function (assert) { 48 | // assert.dom('[data-test-settings-menu] input').exists({ count: 7 }); 49 | // }); 50 | // test('start on startup is not checked by default', async function (assert) { 51 | // assert.dom('[data-test-settings-startup]').isNotChecked(); 52 | // }); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /tests/acceptance/settings/data-test.ts: -------------------------------------------------------------------------------- 1 | import { click, currentURL, visit } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | import sinon from 'sinon'; 4 | 5 | import IDBExportImport from 'indexeddb-export-import'; 6 | 7 | import { resetStorage, waitForAll } from 'swach/tests/helpers'; 8 | import { setupApplicationTest } from 'swach/tests/helpers/index'; 9 | 10 | module('Acceptance | settings/data', function (hooks) { 11 | setupApplicationTest(hooks); 12 | resetStorage(hooks, { seed: { source: 'backup', scenario: 'basic' } }); 13 | 14 | test('visiting /settings/data', async function (assert) { 15 | await visit('/settings/data'); 16 | 17 | assert.strictEqual(currentURL(), '/settings/data'); 18 | }); 19 | 20 | test('changing formats', async function (assert) { 21 | await visit('/settings/data'); 22 | 23 | assert 24 | .dom('[data-test-settings-format-dropdown] [data-test-options-trigger]') 25 | .hasText('hex'); 26 | await click( 27 | '[data-test-settings-format-dropdown] [data-test-options-trigger]', 28 | ); 29 | 30 | await waitForAll(); 31 | 32 | await click( 33 | '[data-test-settings-format-dropdown] [data-test-options-content] [data-test-format-option="hsl"]', 34 | ); 35 | 36 | assert 37 | .dom('[data-test-settings-format-dropdown] [data-test-options-trigger]') 38 | .hasText('hsl'); 39 | }); 40 | 41 | // Electron specific tests 42 | if (typeof requireNode !== 'undefined') { 43 | test('export triggers success message', async function (assert) { 44 | await visit('/settings/data'); 45 | 46 | sinon.stub(IDBExportImport, 'exportToJsonString').callsArg(1); 47 | await click('[data-test-export-swatches-button]'); 48 | await waitForAll(); 49 | assert.dom('.alert.alert-success').exists({ count: 1 }); 50 | }); 51 | test('export triggers error message', async function (assert) { 52 | await visit('/settings/data'); 53 | 54 | sinon 55 | .stub(IDBExportImport, 'exportToJsonString') 56 | .callsArgWith(1, 'error'); 57 | await click('[data-test-export-swatches-button]'); 58 | await waitForAll(); 59 | assert.dom('.alert.alert-danger').exists({ count: 1 }); 60 | }); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /tests/acceptance/welcome-test.ts: -------------------------------------------------------------------------------- 1 | import { click, currentURL, visit } from '@ember/test-helpers'; 2 | import { setupApplicationTest } from 'ember-qunit'; 3 | import { module, test } from 'qunit'; 4 | 5 | import { resetStorage, waitForAll } from 'swach/tests/helpers'; 6 | 7 | module('Acceptance | welcome', function (hooks) { 8 | setupApplicationTest(hooks); 9 | resetStorage(hooks); 10 | 11 | test('welcome flow', async function (assert) { 12 | await visit('/welcome'); 13 | 14 | assert.strictEqual(currentURL(), '/welcome'); 15 | 16 | await click('[data-test-link-auto-start]'); 17 | await waitForAll(); 18 | 19 | assert.strictEqual(currentURL(), '/welcome/auto-start'); 20 | 21 | assert 22 | .dom('[data-test-auto-start-toggle]') 23 | .hasProperty('ariaPressed', 'false'); 24 | 25 | await click('[data-test-auto-start-toggle]'); 26 | 27 | await waitForAll(); 28 | 29 | assert 30 | .dom('[data-test-auto-start-toggle]') 31 | .hasProperty('ariaPressed', 'true'); 32 | 33 | await click('[data-test-link-dock-icon]'); 34 | await waitForAll(); 35 | 36 | assert.strictEqual(currentURL(), '/welcome/dock-icon'); 37 | 38 | assert 39 | .dom('[data-test-show-dock-icon-toggle]') 40 | .hasProperty('ariaPressed', 'false'); 41 | 42 | await click('[data-test-show-dock-icon-toggle]'); 43 | await waitForAll(); 44 | 45 | assert 46 | .dom('[data-test-show-dock-icon-toggle]') 47 | .hasProperty('ariaPressed', 'true'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getContext, settled } from '@ember/test-helpers'; 2 | import { animationsSettled } from 'ember-animated/test-support'; 3 | import { waitForSource } from 'ember-orbit/test-support'; 4 | 5 | import type Owner from '@ember/owner'; 6 | 7 | import type Coordinator from '@orbit/coordinator'; 8 | import type { IndexedDBSource } from '@orbit/indexeddb'; 9 | import type BucketClass from '@orbit/indexeddb-bucket'; 10 | 11 | // @ts-expect-error TODO: not yet typed 12 | import seedOrbit from './orbit/seed'; 13 | 14 | export async function waitForAll() { 15 | const { owner } = getContext() as { owner: Owner }; 16 | // @ts-expect-error Not sure why it says this does not exist 17 | const { services } = owner.resolveRegistration('ember-orbit:config') as { 18 | services: { 19 | coordinator: string; 20 | }; 21 | }; 22 | const coordinator = owner.lookup( 23 | `service:${services.coordinator}`, 24 | ) as unknown as Coordinator; 25 | 26 | for (const source of coordinator.sources) { 27 | await waitForSource(source); 28 | } 29 | 30 | await settled(); 31 | await animationsSettled(); 32 | } 33 | 34 | export function resetStorage( 35 | hooks: NestedHooks, 36 | options: { seed?: { source?: string; scenario?: string } } = {}, 37 | ) { 38 | hooks.beforeEach(async function () { 39 | if (options.seed) { 40 | const sourceName = options.seed.source ?? 'backup'; 41 | const source = this.owner.lookup(`data-source:${sourceName}`); 42 | 43 | await seedOrbit(source, options.seed.scenario); 44 | } 45 | }); 46 | 47 | hooks.afterEach(async function () { 48 | const backup = this.owner.lookup('data-source:backup') as IndexedDBSource; 49 | 50 | await backup.cache.deleteDB(); 51 | 52 | const bucket = this.owner.lookup('data-bucket:main') as 53 | | BucketClass 54 | | undefined; 55 | 56 | await bucket?.clear(); 57 | 58 | self.localStorage.removeItem('storage:settings'); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /tests/helpers/flash-message.js: -------------------------------------------------------------------------------- 1 | import FlashObject from 'ember-cli-flash/flash/object'; 2 | 3 | FlashObject.reopen({ init() {} }); 4 | -------------------------------------------------------------------------------- /tests/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type SetupTestOptions, 3 | setupApplicationTest as upstreamSetupApplicationTest, 4 | setupRenderingTest as upstreamSetupRenderingTest, 5 | setupTest as upstreamSetupTest, 6 | } from 'ember-qunit'; 7 | 8 | // This file exists to provide wrappers around ember-qunit's 9 | // test setup functions. This way, you can easily extend the setup that is 10 | // needed per test type. 11 | 12 | function setupApplicationTest(hooks: NestedHooks, options?: SetupTestOptions) { 13 | upstreamSetupApplicationTest(hooks, options); 14 | 15 | // Additional setup for application tests can be done here. 16 | // 17 | // For example, if you need an authenticated session for each 18 | // application test, you could do: 19 | // 20 | // hooks.beforeEach(async function () { 21 | // await authenticateSession(); // ember-simple-auth 22 | // }); 23 | // 24 | // This is also a good place to call test setup functions coming 25 | // from other addons: 26 | // 27 | // setupIntl(hooks, 'en-us'); // ember-intl 28 | // setupMirage(hooks); // ember-cli-mirage 29 | } 30 | 31 | function setupRenderingTest(hooks: NestedHooks, options?: SetupTestOptions) { 32 | upstreamSetupRenderingTest(hooks, options); 33 | 34 | // Additional setup for rendering tests can be done here. 35 | } 36 | 37 | function setupTest(hooks: NestedHooks, options?: SetupTestOptions) { 38 | upstreamSetupTest(hooks, options); 39 | 40 | // Additional setup for unit tests can be done here. 41 | } 42 | 43 | export { setupApplicationTest, setupRenderingTest, setupTest }; 44 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swach Tests 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{content-for "body-footer"}} 37 | {{content-for "test-body-footer"}} 38 | 39 | 40 | -------------------------------------------------------------------------------- /tests/integration/components/contrast-checker-test.js: -------------------------------------------------------------------------------- 1 | import { fillIn, render, triggerKeyEvent } from '@ember/test-helpers'; 2 | import { hbs } from 'ember-cli-htmlbars'; 3 | import { module, test } from 'qunit'; 4 | 5 | import { waitForAll } from 'swach/tests/helpers'; 6 | import { setupRenderingTest } from 'swach/tests/helpers/index'; 7 | 8 | module('Integration | Component | contrast-checker', function (hooks) { 9 | setupRenderingTest(hooks); 10 | 11 | test('WCAG - Score and string calculated', async function (assert) { 12 | await render(hbs``); 13 | 14 | await waitForAll(); 15 | 16 | await fillIn('[data-test-bg-input]', '#004747'); 17 | await triggerKeyEvent('[data-test-bg-input]', 'keypress', 13); 18 | await fillIn('[data-test-fg-input]', '#005A2A'); 19 | await triggerKeyEvent('[data-test-fg-input]', 'keypress', 13); 20 | 21 | await waitForAll(); 22 | 23 | assert.dom('[data-test-wcag-score]').hasText('1.25'); 24 | assert.dom('[data-test-wcag-string]').hasText('Fail'); 25 | assert 26 | .dom('[data-test-contrast-preview]') 27 | .hasStyle({ backgroundColor: 'rgb(0, 71, 71)', color: 'rgb(0, 90, 42)' }); 28 | 29 | await fillIn('[data-test-fg-input]', '#00A24B'); 30 | await triggerKeyEvent('[data-test-fg-input]', 'keypress', 13); 31 | 32 | await waitForAll(); 33 | 34 | assert.dom('[data-test-wcag-score]').hasText('3.15'); 35 | assert.dom('[data-test-wcag-string]').hasText('AA Large'); 36 | assert.dom('[data-test-contrast-preview]').hasStyle({ 37 | backgroundColor: 'rgb(0, 71, 71)', 38 | color: 'rgb(0, 162, 75)', 39 | }); 40 | 41 | await fillIn('[data-test-fg-input]', '#00CE60'); 42 | await triggerKeyEvent('[data-test-fg-input]', 'keypress', 13); 43 | 44 | await waitForAll(); 45 | 46 | assert.dom('[data-test-wcag-score]').hasText('5.02'); 47 | assert.dom('[data-test-wcag-string]').hasText('AA'); 48 | assert.dom('[data-test-contrast-preview]').hasStyle({ 49 | backgroundColor: 'rgb(0, 71, 71)', 50 | color: 'rgb(0, 206, 96)', 51 | }); 52 | 53 | await fillIn('[data-test-fg-input]', '#FFFFFF'); 54 | await triggerKeyEvent('[data-test-fg-input]', 'keypress', 13); 55 | 56 | await waitForAll(); 57 | 58 | assert.dom('[data-test-wcag-score]').hasText('10.54'); 59 | assert.dom('[data-test-wcag-string]').hasText('AAA'); 60 | assert.dom('[data-test-contrast-preview]').hasStyle({ 61 | backgroundColor: 'rgb(0, 71, 71)', 62 | color: 'rgb(255, 255, 255)', 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/colors.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'color', 4 | id: 'black2', 5 | attributes: { name: 'Black', r: 0, g: 0, b: 0, a: 1 }, 6 | }, 7 | { 8 | type: 'color', 9 | id: 'denim', 10 | attributes: { name: 'Denim', r: 53, g: 109, b: 196, a: 1 }, 11 | }, 12 | { 13 | type: 'color', 14 | id: 'inch-worm', 15 | attributes: { name: 'Inch Worm', r: 176, g: 245, b: 102, a: 1 }, 16 | }, 17 | { 18 | type: 'color', 19 | id: 'pale-magenta', 20 | attributes: { name: 'Pale Magenta', r: 247, g: 138, b: 224, a: 1 }, 21 | }, 22 | { 23 | type: 'color', 24 | id: 'shamrock', 25 | attributes: { name: 'Shamrock', r: 74, g: 242, b: 161, a: 1 }, 26 | }, 27 | { 28 | type: 'color', 29 | id: 'white', 30 | attributes: { name: 'White', r: 255, g: 255, b: 255, a: 1 }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/colors/color-history.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'color', 4 | id: 'black', 5 | attributes: { name: 'Black', r: 0, g: 0, b: 0, a: 1 }, 6 | }, 7 | { 8 | type: 'color', 9 | attributes: { name: 'Pale Magenta', r: 247, g: 138, b: 224, a: 1 }, 10 | }, 11 | { 12 | type: 'color', 13 | attributes: { name: 'Shamrock', r: 74, g: 242, b: 161, a: 1 }, 14 | }, 15 | { 16 | type: 'color', 17 | attributes: { name: 'White', r: 255, g: 255, b: 255, a: 1 }, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/colors/first-palette.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'color', 4 | attributes: { name: 'Black', r: 0, g: 0, b: 0, a: 1 }, 5 | }, 6 | { 7 | type: 'color', 8 | attributes: { name: 'Denim', r: 53, g: 109, b: 196, a: 1 }, 9 | }, 10 | { 11 | type: 'color', 12 | attributes: { name: 'Inch Worm', r: 176, g: 245, b: 102, a: 1 }, 13 | }, 14 | { 15 | type: 'color', 16 | attributes: { name: 'White', r: 255, g: 255, b: 255, a: 1 }, 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/colors/locked-palette.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'color', 4 | attributes: { name: 'Black', r: 0, g: 0, b: 0, a: 1 }, 5 | }, 6 | { 7 | type: 'color', 8 | attributes: { name: 'Shamrock', r: 74, g: 242, b: 161, a: 1 }, 9 | }, 10 | { 11 | type: 'color', 12 | attributes: { name: 'White', r: 255, g: 255, b: 255, a: 1 }, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/colors/second-palette.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'color', 4 | attributes: { name: 'Black', r: 0, g: 0, b: 0, a: 1 }, 5 | }, 6 | { 7 | type: 'color', 8 | attributes: { name: 'White', r: 255, g: 255, b: 255, a: 1 }, 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/orbit/fixtures/palettes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | type: 'palette', 4 | id: 'color-history-123', 5 | attributes: { 6 | name: 'Color History', 7 | colorOrder: [], 8 | isColorHistory: true, 9 | isFavorite: false, 10 | isLocked: false, 11 | }, 12 | }, 13 | { 14 | type: 'palette', 15 | id: 'first-palette', 16 | attributes: { 17 | name: 'First Palette', 18 | colorOrder: [], 19 | index: 0, 20 | isColorHistory: false, 21 | isFavorite: false, 22 | isLocked: false, 23 | }, 24 | }, 25 | { 26 | type: 'palette', 27 | id: 'second-palette', 28 | attributes: { 29 | name: 'Second Palette', 30 | colorOrder: [], 31 | index: 1, 32 | isColorHistory: false, 33 | isFavorite: false, 34 | isLocked: false, 35 | }, 36 | }, 37 | { 38 | type: 'palette', 39 | id: 'locked-palette', 40 | attributes: { 41 | name: 'Locked Palette', 42 | colorOrder: [], 43 | index: 2, 44 | isColorHistory: false, 45 | isFavorite: false, 46 | isLocked: true, 47 | }, 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import { setApplication } from '@ember/test-helpers'; 2 | import { setupEmberOnerrorValidation, start } from 'ember-qunit'; 3 | import { loadTests } from 'ember-qunit/test-loader'; 4 | import setupSinon from 'ember-sinon-qunit'; 5 | import * as QUnit from 'qunit'; 6 | import { setup } from 'qunit-dom'; 7 | 8 | import './helpers/flash-message'; 9 | import Application from 'swach/app'; 10 | import config from 'swach/config/environment'; 11 | 12 | setApplication(Application.create(config.APP)); 13 | 14 | setup(QUnit.assert); 15 | setupSinon(); 16 | setupEmberOnerrorValidation(); 17 | loadTests(); 18 | start(); 19 | -------------------------------------------------------------------------------- /tests/unit/utils/remove-from-to-test.ts: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | 3 | import removeFromTo from 'swach/utils/remove-from-to'; 4 | 5 | module('Unit | Utility | removeFromTo', function () { 6 | test('from: 0, to: 0', function (assert) { 7 | const array = ['a', 'b', 'c', 'd']; 8 | const result = removeFromTo(array, 0, 0); 9 | 10 | assert.strictEqual(result, 3); 11 | assert.deepEqual(array, ['b', 'c', 'd']); 12 | }); 13 | 14 | test('from: 2, to: 0', function (assert) { 15 | const array = ['a', 'b', 'c', 'd']; 16 | const result = removeFromTo(array, 2, 0); 17 | 18 | assert.strictEqual(result, 3); 19 | assert.deepEqual(array, ['a', 'b', 'd']); 20 | }); 21 | 22 | test('from: 0, to: 2', function (assert) { 23 | const array = ['a', 'b', 'c', 'd']; 24 | const result = removeFromTo(array, 0, 2); 25 | 26 | assert.strictEqual(result, 1); 27 | assert.deepEqual(array, ['d']); 28 | }); 29 | 30 | test('from: 1, to: 2', function (assert) { 31 | const array = ['a', 'b', 'c', 'd']; 32 | const result = removeFromTo(array, 1, 2); 33 | 34 | assert.strictEqual(result, 2); 35 | assert.deepEqual(array, ['a', 'd']); 36 | }); 37 | 38 | test('from: -1, to: 2', function (assert) { 39 | const array = ['a', 'b', 'c', 'd']; 40 | const result = removeFromTo(array, -1, 2); 41 | 42 | assert.strictEqual(result, 4); 43 | assert.deepEqual(array, ['a', 'b', 'c', 'd']); 44 | }); 45 | 46 | test('from: 2, to: -2', function (assert) { 47 | const array = ['a', 'b', 'c', 'd']; 48 | const result = removeFromTo(array, 2, -2); 49 | 50 | assert.strictEqual(result, 3); 51 | assert.deepEqual(array, ['a', 'b', 'd']); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember", 3 | "glint": { "environment": ["ember-loose", "ember-template-imports"] }, 4 | "compilerOptions": { 5 | // The combination of `baseUrl` with `paths` allows Ember's classic package 6 | // layout, which is not resolvable with the Node resolution algorithm, to 7 | // work with TypeScript. 8 | "baseUrl": ".", 9 | "paths": { 10 | "swach/tests/*": ["tests/*"], 11 | "swach/*": ["app/*"], 12 | "*": ["types/*"] 13 | }, 14 | "types": ["ember-source/types", "node"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /types/electron/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'electron' { 2 | export interface IpcRenderer { 3 | invoke: (channel: string, ...args) => Promise; 4 | on: (channel: string, listener: (event: any, ...args) => void) => void; 5 | removeAllListeners: (channel: string) => void; 6 | send: (channel: string, ...args) => void; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /types/ember-animated/motions/move.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-animated/motions/move' { 2 | export = function move(sprite: unknown, options: { easing: unknown }) {}; 3 | } 4 | -------------------------------------------------------------------------------- /types/ember-animated/motions/opacity.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-animated/motions/opacity' { 2 | export declare function fadeOut( 3 | sprite: unknown, 4 | options: { easing: unknown } 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /types/ember-animated/transitions/fade/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-animated/transitions/fade' {} 2 | -------------------------------------------------------------------------------- /types/ember-cognito/services/cognito.d.ts: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | 3 | declare module 'ember-cognito/services/cognito' { 4 | export default class CognitoService extends Service { 5 | user: { attributes: { email: string; email_verified: boolean } }; 6 | confirmSignUp(username: string, code: string); 7 | forgotPassword(username: string): unknown; 8 | forgotPasswordSubmit( 9 | username: string, 10 | code: string, 11 | password: string 12 | ): unknown; 13 | signUp(username: string, password: string, attributes: unknown): unknown; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /types/ember-cognito/test-support/index.d.ts: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | declare module 'ember-cognito/test-support' { 3 | export const MockAuth = EmberObject.extend({ 4 | async confirmSignUp(username: string, confirmationCode: string) {}, 5 | signUp() {} 6 | }); 7 | export const MockUser = EmberObject.extend({ create() {} }); 8 | export const mockAuth = any; 9 | export declare function mockCognitoUser(options: {} = {}): MockUser; 10 | } 11 | -------------------------------------------------------------------------------- /types/ember-drag-sort/components/drag-sort-list.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-drag-sort/components/drag-sort-list' { 2 | import EmberObject from '@ember/object'; 3 | 4 | export default class DragSortList extends EmberObject { 5 | dragEnter(event: Event): void {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /types/ember-drag-sort/services/drag-sort.d.ts: -------------------------------------------------------------------------------- 1 | import Service from '@ember/service'; 2 | import EventedMixin from '@ember/object/evented' 3 | 4 | declare module 'ember-drag-sort/services/drag-sort' { 5 | export default class DragSortService extends Service.extend(EventedMixin) { 6 | } 7 | } -------------------------------------------------------------------------------- /types/ember-local-storage/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-local-storage' { 2 | export declare function storageFor( 3 | key: string, 4 | modelName?: string, 5 | options: {} = {} 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /types/ember-local-storage/local/object.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-local-storage/local/object' { 2 | import EmberObject from '@ember/object'; 3 | export default class LocalObject extends EmberObject {} 4 | } 5 | -------------------------------------------------------------------------------- /types/ember-local-storage/test-support/reset-storage.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-local-storage/test-support/reset-storage' { 2 | export = function resetStorages(): void {}; 3 | } -------------------------------------------------------------------------------- /types/ember-sinon-qunit/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-sinon-qunit' { 2 | export = function setupSinon(): void {}; 3 | } 4 | -------------------------------------------------------------------------------- /types/indexeddb-export-import/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'indexeddb-export-import' { 2 | export declare function exportToJsonString( 3 | idbDatabase: IDBDatabase, 4 | cb: (error: Event | null, jsonString: string) => void 5 | ); 6 | export declare function importFromJsonString( 7 | idbDatabase: IDBDatabase, 8 | jsonString: string, 9 | cb: (error: Event | null) => void 10 | ); 11 | export declare function clearDatabase( 12 | idbDatabase: IDBDatabase, 13 | cb: (error: Event | null) => void 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /types/nearest-color/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'nearest-color' { 2 | export function from( 3 | availableColors: Array | Object 4 | ): (string) => ColorMatch | string; 5 | } 6 | -------------------------------------------------------------------------------- /types/swach/index.d.ts: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | declare global { 4 | interface Array extends Ember.ArrayPrototypeExtensions {} 5 | // interface Function extends Ember.FunctionPrototypeExtensions {} 6 | } 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /types/throttle-debounce/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'throttle-debounce' { 2 | export function debounce( 3 | delay: number, 4 | atBegin?: boolean, 5 | callback?: Function 6 | ); 7 | export function debounce(delay: number, callback?: Function); 8 | } 9 | -------------------------------------------------------------------------------- /types/wcag-contrast/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'wcag-contrast' { 2 | export function hex( 3 | backgroundColorHex: string, 4 | foregroundColorHex?: string, 5 | ): number; 6 | /** 7 | * Takes a score value like 2.0 and returns the score like AAA 8 | */ 9 | export function score(hexScoreNumberString: string): string; 10 | } 11 | --------------------------------------------------------------------------------