├── .browserslistrc ├── .codecov.yml ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── misc └── sketch-example.png ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── footer.component.ts │ └── package.json ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── lib │ ├── alpha.component.ts │ ├── alpha │ │ ├── alpha-picker.component.ts │ │ ├── alpha.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── block │ │ ├── block-swatches.component.ts │ │ ├── block.component.ts │ │ ├── block.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── checkboard.component.ts │ ├── chrome │ │ ├── chrome-fields.component.ts │ │ ├── chrome.component.ts │ │ ├── chrome.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── circle │ │ ├── circle-swatch.component.ts │ │ ├── circle.component.ts │ │ ├── circle.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── color-wrap.component.ts │ ├── compact │ │ ├── compact-color.component.ts │ │ ├── compact-fields.component.ts │ │ ├── compact.component.ts │ │ ├── compact.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── coordinates.directive.ts │ ├── editable-input.component.ts │ ├── github │ │ ├── github-swatch.component.ts │ │ ├── github.component.ts │ │ ├── github.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── helpers │ │ ├── checkboard.ts │ │ ├── color.interfaces.ts │ │ └── color.ts │ ├── hue.component.ts │ ├── hue │ │ ├── hue-picker.component.ts │ │ ├── hue.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── material │ │ ├── material.component.ts │ │ ├── material.spec.ts │ │ ├── ng-package.json │ │ ├── package.json │ │ └── public_api.ts │ ├── ng-package.json │ ├── package.json │ ├── photoshop │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── photoshop-button.component.ts │ │ ├── photoshop-fields.component.ts │ │ ├── photoshop-previews.component.ts │ │ ├── photoshop.component.ts │ │ ├── photoshop.spec.ts │ │ └── public_api.ts │ ├── public_api.ts │ ├── raised.component.ts │ ├── saturation.component.ts │ ├── shade.component.ts │ ├── shade │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── public_api.ts │ │ ├── shade-picker.component.ts │ │ └── shade-picker.spec.ts │ ├── sketch │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── public_api.ts │ │ ├── sketch-fields.component.ts │ │ ├── sketch-preset-colors.component.ts │ │ ├── sketch.component.ts │ │ └── sketch.spec.ts │ ├── slider │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── public_api.ts │ │ ├── slider-swatch.component.ts │ │ ├── slider-swatches.component.ts │ │ ├── slider.component.ts │ │ └── slider.spec.ts │ ├── swatch.component.ts │ ├── swatches │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── public_api.ts │ │ ├── swatches-color.component.ts │ │ ├── swatches-group.component.ts │ │ ├── swatches.component.ts │ │ └── swatches.spec.ts │ └── twitter │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── public_api.ts │ │ ├── twitter.component.ts │ │ └── twitter.spec.ts ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts └── types.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── vercel.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 1 Safari major versions 14 | not IE 11 15 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: '50..100' 3 | status: 4 | project: no 5 | patch: no 6 | comment: 7 | require_changes: yes 8 | behavior: once 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "plugin:@angular-eslint/template/process-inline-templates" 14 | ], 15 | "rules": { 16 | "@angular-eslint/no-output-on-prefix": "off", 17 | "@angular-eslint/component-class-suffix": "off", 18 | "@angular-eslint/prefer-standalone": "off" 19 | } 20 | }, 21 | { 22 | "files": ["*.html"], 23 | "extends": ["plugin:@angular-eslint/template/recommended"], 24 | "rules": {} 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: 22 17 | cache: 'npm' 18 | - run: npm ci 19 | - name: lint 20 | run: npm run lint 21 | - run: npm run build 22 | - name: test 23 | run: npm run test:ci 24 | - name: coverage 25 | uses: codecov/codecov-action@v3 26 | with: 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | 29 | publish: 30 | needs: build 31 | runs-on: ubuntu-latest 32 | if: github.ref_name == 'master' 33 | permissions: 34 | contents: write # to be able to publish a GitHub release 35 | issues: write # to be able to comment on released issues 36 | pull-requests: write # to be able to comment on released pull requests 37 | id-token: write # to enable use of OIDC for npm provenance 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-node@v4 41 | with: 42 | node-version: 22 43 | cache: 'npm' 44 | - run: npm ci 45 | - run: npm run build 46 | - name: release 47 | run: cd dist && npx semantic-release 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid", 8 | "tabWidth": 2 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Scott Cooper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-color": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "css" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:application", 22 | "options": { 23 | "outputPath": { 24 | "base": "dist" 25 | }, 26 | "index": "src/index.html", 27 | "polyfills": ["src/polyfills.ts"], 28 | "tsConfig": "tsconfig.app.json", 29 | "assets": [ 30 | { 31 | "glob": "**/*", 32 | "input": "public" 33 | }, 34 | "src/assets" 35 | ], 36 | "styles": ["src/styles.css"], 37 | "scripts": [], 38 | "extractLicenses": false, 39 | "sourceMap": true, 40 | "optimization": false, 41 | "namedChunks": true, 42 | "browser": "src/main.ts" 43 | }, 44 | "configurations": { 45 | "production": { 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "optimization": true, 53 | "outputHashing": "all", 54 | "sourceMap": false, 55 | "namedChunks": false, 56 | "extractLicenses": true, 57 | "budgets": [ 58 | { 59 | "type": "initial", 60 | "maximumWarning": "500kb", 61 | "maximumError": "1mb" 62 | }, 63 | { 64 | "type": "anyComponentStyle", 65 | "maximumWarning": "2kb", 66 | "maximumError": "4kb" 67 | } 68 | ] 69 | } 70 | } 71 | }, 72 | "serve": { 73 | "builder": "@angular-devkit/build-angular:dev-server", 74 | "options": { 75 | "buildTarget": "ngx-color:build" 76 | }, 77 | "configurations": { 78 | "production": { 79 | "buildTarget": "ngx-color:build:production" 80 | } 81 | } 82 | }, 83 | "extract-i18n": { 84 | "builder": "@angular-devkit/build-angular:extract-i18n", 85 | "options": { 86 | "buildTarget": "ngx-color:build" 87 | } 88 | }, 89 | "test": { 90 | "builder": "@angular-devkit/build-angular:karma", 91 | "options": { 92 | "main": "src/test.ts", 93 | "polyfills": "src/polyfills.ts", 94 | "tsConfig": "tsconfig.spec.json", 95 | "karmaConfig": "karma.conf.js", 96 | "assets": [ 97 | { 98 | "glob": "**/*", 99 | "input": "public" 100 | }, 101 | "src/assets" 102 | ], 103 | "styles": ["src/styles.css"], 104 | "scripts": [] 105 | } 106 | }, 107 | "lint": { 108 | "builder": "@angular-eslint/builder:lint", 109 | "options": { 110 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "cli": { 117 | "analytics": false 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/zzz'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true, 22 | }, 23 | reporters: ['coverage-istanbul', 'progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | customLaunchers: { 30 | ChromeHeadlessCustom: { 31 | base: 'ChromeHeadless', 32 | flags: ['--no-sandbox', '--disable-gpu'], 33 | }, 34 | }, 35 | singleRun: false, 36 | restartOnFileChange: true, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /misc/sketch-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-color/c2d3742e098bc7ea14bb7e44d80ea88233efe0af/misc/sketch-example.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "homepage": "https://github.com/scttcper/ngx-color", 6 | "bugs": "https://github.com/scttcper/ngx-color/issues", 7 | "repository": "scttcper/ngx-color", 8 | "scripts": { 9 | "ng": "ng", 10 | "start": "ng serve", 11 | "build": "ng-packagr -p ./src/lib/ng-package.json && cp README.md ./dist && cp LICENSE ./dist", 12 | "ghpages": "ng build --configuration production --no-progress", 13 | "test": "ng test --watch=false --code-coverage", 14 | "test:watch": "ng test", 15 | "test:ci": "ng test --watch=false --code-coverage --no-progress --browsers=ChromeHeadlessCustom", 16 | "lint": "ng lint && prettier --check .", 17 | "lint:fix": "ng lint --fix && prettier --write .", 18 | "format": "prettier --write ." 19 | }, 20 | "private": true, 21 | "dependencies": { 22 | "@ctrl/tinycolor": "4.1.0", 23 | "material-colors": "1.2.6" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "19.1.4", 27 | "@angular-eslint/builder": "19.0.2", 28 | "@angular-eslint/eslint-plugin": "19.0.2", 29 | "@angular-eslint/eslint-plugin-template": "19.0.2", 30 | "@angular-eslint/template-parser": "19.0.2", 31 | "@angular/animations": "19.1.3", 32 | "@angular/cli": "19.1.4", 33 | "@angular/common": "19.1.3", 34 | "@angular/compiler": "19.1.3", 35 | "@angular/compiler-cli": "19.1.3", 36 | "@angular/core": "19.1.3", 37 | "@angular/forms": "19.1.3", 38 | "@angular/language-service": "19.1.3", 39 | "@angular/platform-browser": "19.1.3", 40 | "@angular/platform-browser-dynamic": "19.1.3", 41 | "@angular/router": "19.1.3", 42 | "@ctrl/ngx-github-buttons": "9.0.0", 43 | "@types/fs-extra": "11.0.4", 44 | "@types/jasmine": "5.1.5", 45 | "@types/node": "22.12.0", 46 | "@typescript-eslint/eslint-plugin": "8.22.0", 47 | "@typescript-eslint/parser": "8.22.0", 48 | "bootstrap": "5.3.3", 49 | "core-js": "3.30.2", 50 | "del": "8.0.0", 51 | "eslint": "9.19.0", 52 | "fs-extra": "11.3.0", 53 | "jasmine-core": "5.5.0", 54 | "karma": "6.4.4", 55 | "karma-chrome-launcher": "3.2.0", 56 | "karma-cli": "2.0.0", 57 | "karma-coverage-istanbul-reporter": "3.0.3", 58 | "karma-jasmine": "5.1.0", 59 | "karma-jasmine-html-reporter": "2.1.0", 60 | "ng-packagr": "19.1.2", 61 | "prettier": "3.2.5", 62 | "puppeteer": "20.2.0", 63 | "rxjs": "7.8.1", 64 | "tslib": "2.8.1", 65 | "typescript": "5.7.3", 66 | "zone.js": "0.15.0" 67 | }, 68 | "release": { 69 | "branches": [ 70 | "master" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-color/c2d3742e098bc7ea14bb7e44d80ea88233efe0af/public/favicon.ico -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .home { 2 | position: absolute; 3 | display: block; 4 | } 5 | 6 | .cover { 7 | bottom: 0; 8 | left: 0; 9 | opacity: 0.5; 10 | position: absolute; 11 | right: 0; 12 | top: 0; 13 | transition: background-color 100ms linear; 14 | } 15 | 16 | .title { 17 | color: rgba(0, 0, 0, 0.65); 18 | font-size: 52px; 19 | padding-top: 70px; 20 | } 21 | 22 | .description { 23 | color: rgba(0, 0, 0, 0.6); 24 | font-size: 20px; 25 | font-weight: 300; 26 | line-height: 27px; 27 | padding-top: 15px; 28 | } 29 | .description > a { 30 | color: rgba(0, 0, 0, 0.6); 31 | text-decoration: underline; 32 | } 33 | 34 | .label { 35 | text-align: center; 36 | width: 100%; 37 | color: rgba(0, 0, 0, 0.4); 38 | font-size: 12px; 39 | margin-top: 5px; 40 | } 41 | .white-label { 42 | width: 100%; 43 | font-size: 12px; 44 | margin-top: 10px; 45 | color: rgba(255, 255, 255, 0.7); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
Angular Color
8 |
9 | Port of react-color by 10 | casesandberg 11 |
12 |
13 | A Collection of Color Pickers from Sketch, Photoshop, Chrome, Github, Twitter, Material 14 | Design & more 15 |
16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 |
Chrome
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 |
Sketch
35 |
36 |
37 |
38 |
39 | 40 | 41 |
Photoshop
42 |
43 |
44 |
45 |
46 |
47 |
48 | 53 |
54 |
55 |
Block
56 |
57 |
58 |
59 |
60 |
61 |
62 | 68 |
69 |
70 |
Github
71 |
72 |
73 |
74 |
75 | 80 |
Hue
81 |
82 |
83 |
84 |
85 | 90 |
Alpha
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | 105 |
Twitter
106 |
107 |
108 |
109 |
110 |
111 |
112 | 117 |
Circle
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 133 |
Slider
134 |
135 |
136 |
137 |
138 |
139 |
140 | 145 |
Compact
146 |
147 |
148 |
149 |
150 |
151 |
152 | 157 |
Material
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 171 |
Swatches
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 | 185 |
Shade Slider
186 |
187 |
188 |
189 |
190 | 191 | 192 |
193 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { AppComponent } from './app.component'; 4 | import { AppModule } from './app.module'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(waitForAsync(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [], 10 | imports: [AppModule], 11 | }).compileComponents(); 12 | })); 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | fixture.detectChanges(); 17 | expect(app).toBeTruthy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { ColorEvent } from 'ngx-color'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'], 9 | standalone: false, 10 | }) 11 | export class AppComponent { 12 | title = 'app'; 13 | primaryColor = '#194D33'; 14 | state = { 15 | h: 150, 16 | s: 0.5, 17 | l: 0.2, 18 | a: 1, 19 | }; 20 | 21 | changeComplete($event: ColorEvent): void { 22 | this.state = $event.color.hsl; 23 | this.primaryColor = $event.color.hex; 24 | console.log('changeComplete', $event); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { GhButtonModule } from '@ctrl/ngx-github-buttons'; 5 | 6 | import { ColorAlphaModule } from '../lib/alpha/alpha-picker.component'; 7 | import { ColorBlockModule } from '../lib/block/block.component'; 8 | import { ColorChromeModule } from '../lib/chrome/chrome.component'; 9 | import { ColorCircleModule } from '../lib/circle/circle.component'; 10 | import { ColorCompactModule } from '../lib/compact/compact.component'; 11 | import { ColorGithubModule } from '../lib/github/github.component'; 12 | import { ColorHueModule } from '../lib/hue/hue-picker.component'; 13 | import { ColorMaterialModule } from '../lib/material/material.component'; 14 | import { ColorPhotoshopModule } from '../lib/photoshop/photoshop.component'; 15 | import { ColorSketchModule } from '../lib/sketch/sketch.component'; 16 | import { ColorSliderModule } from '../lib/slider/slider.component'; 17 | import { ColorSwatchesModule } from '../lib/swatches/swatches.component'; 18 | import { ColorTwitterModule } from '../lib/twitter/twitter.component'; 19 | import { AppComponent } from './app.component'; 20 | import { FooterComponent } from './footer.component'; 21 | import { ColorShadeModule } from '../lib/shade/shade-picker.component'; 22 | 23 | @NgModule({ 24 | declarations: [AppComponent, FooterComponent], 25 | imports: [ 26 | BrowserModule, 27 | 28 | GhButtonModule, 29 | 30 | ColorAlphaModule, 31 | ColorBlockModule, 32 | ColorChromeModule, 33 | ColorCircleModule, 34 | ColorCompactModule, 35 | ColorGithubModule, 36 | ColorHueModule, 37 | ColorMaterialModule, 38 | ColorPhotoshopModule, 39 | ColorSketchModule, 40 | ColorSliderModule, 41 | ColorSwatchesModule, 42 | ColorTwitterModule, 43 | ColorShadeModule, 44 | ], 45 | providers: [], 46 | bootstrap: [AppComponent], 47 | }) 48 | export class AppModule {} 49 | -------------------------------------------------------------------------------- /src/app/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, VERSION } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | template: ` 6 | 13 | `, 14 | styles: [ 15 | ` 16 | .footer { 17 | line-height: 2; 18 | text-align: center; 19 | font-size: 70%; 20 | color: #999; 21 | font-family: var(--font-family-monospace); 22 | } 23 | `, 24 | ], 25 | standalone: false, 26 | }) 27 | export class FooterComponent { 28 | version = VERSION.full; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color", 3 | "private": true, 4 | "description_1": "This is a special package.json file that is not used by package managers.", 5 | "description_2": "It is used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size.", 6 | "description_3": "It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.", 7 | "description_4": "To learn more about this file see: https://angular.io/config/app-package-json.", 8 | "sideEffects": false 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-color/c2d3742e098bc7ea14bb7e44d80ea88233efe0af/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false, 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Color 7 | 8 | 9 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/lib/alpha.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | Output, 10 | } from '@angular/core'; 11 | 12 | import { CheckboardModule } from './checkboard.component'; 13 | import { CoordinatesModule } from './coordinates.directive'; 14 | import { HSLA, RGBA } from './helpers/color.interfaces'; 15 | 16 | @Component({ 17 | selector: 'color-alpha', 18 | template: ` 19 |
20 |
21 | 22 |
23 |
29 |
34 |
35 |
36 |
37 |
38 |
39 | `, 40 | styles: [ 41 | ` 42 | .alpha { 43 | position: absolute; 44 | top: 0; 45 | bottom: 0; 46 | left: 0; 47 | right: 0; 48 | } 49 | .alpha-checkboard { 50 | position: absolute; 51 | top: 0; 52 | bottom: 0; 53 | left: 0; 54 | right: 0; 55 | overflow: hidden; 56 | } 57 | .alpha-gradient { 58 | position: absolute; 59 | top: 0; 60 | bottom: 0; 61 | left: 0; 62 | right: 0; 63 | } 64 | .alpha-container { 65 | position: relative; 66 | height: 100%; 67 | margin: 0 3px; 68 | } 69 | .alpha-pointer { 70 | position: absolute; 71 | } 72 | .alpha-slider { 73 | width: 4px; 74 | border-radius: 1px; 75 | height: 8px; 76 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); 77 | background: #fff; 78 | margin-top: 1px; 79 | transform: translateX(-2px); 80 | } 81 | `, 82 | ], 83 | changeDetection: ChangeDetectionStrategy.OnPush, 84 | preserveWhitespaces: false, 85 | standalone: false, 86 | }) 87 | export class AlphaComponent implements OnChanges { 88 | @Input() hsl!: HSLA; 89 | @Input() rgb!: RGBA; 90 | @Input() pointer!: Record; 91 | @Input() shadow!: string; 92 | @Input() radius!: number | string; 93 | @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; 94 | @Output() onChange = new EventEmitter(); 95 | gradient!: Record; 96 | pointerLeft!: number; 97 | pointerTop!: number; 98 | 99 | ngOnChanges() { 100 | if (this.direction === 'vertical') { 101 | this.pointerLeft = 0; 102 | this.pointerTop = this.rgb.a * 100; 103 | this.gradient = { 104 | background: `linear-gradient(to bottom, rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b}, 0) 0%, 105 | rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b}, 1) 100%)`, 106 | }; 107 | } else { 108 | this.gradient = { 109 | background: `linear-gradient(to right, rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b}, 0) 0%, 110 | rgba(${this.rgb.r},${this.rgb.g},${this.rgb.b}, 1) 100%)`, 111 | }; 112 | this.pointerLeft = this.rgb.a * 100; 113 | } 114 | } 115 | handleChange({ top, left, containerHeight, containerWidth, $event }): void { 116 | let data: any; 117 | if (this.direction === 'vertical') { 118 | let a: number; 119 | if (top < 0) { 120 | a = 0; 121 | } else if (top > containerHeight) { 122 | a = 1; 123 | } else { 124 | a = Math.round((top * 100) / containerHeight) / 100; 125 | } 126 | 127 | if (this.hsl.a !== a) { 128 | data = { 129 | h: this.hsl.h, 130 | s: this.hsl.s, 131 | l: this.hsl.l, 132 | a, 133 | source: 'rgb', 134 | }; 135 | } 136 | } else { 137 | let a: number; 138 | if (left < 0) { 139 | a = 0; 140 | } else if (left > containerWidth) { 141 | a = 1; 142 | } else { 143 | a = Math.round((left * 100) / containerWidth) / 100; 144 | } 145 | 146 | if (this.hsl.a !== a) { 147 | data = { 148 | h: this.hsl.h, 149 | s: this.hsl.s, 150 | l: this.hsl.l, 151 | a, 152 | source: 'rgb', 153 | }; 154 | } 155 | } 156 | 157 | if (!data) { 158 | return; 159 | } 160 | 161 | this.onChange.emit({ data, $event }); 162 | } 163 | } 164 | 165 | @NgModule({ 166 | declarations: [AlphaComponent], 167 | exports: [AlphaComponent], 168 | imports: [CommonModule, CheckboardModule, CoordinatesModule], 169 | }) 170 | export class AlphaModule {} 171 | -------------------------------------------------------------------------------- /src/lib/alpha/alpha-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | forwardRef, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | } from '@angular/core'; 10 | 11 | import { AlphaModule, CheckboardModule, ColorWrap, toState } from 'ngx-color'; 12 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 13 | 14 | @Component({ 15 | selector: 'color-alpha-picker', 16 | template: ` 17 |
18 | 25 |
26 | `, 27 | styles: [ 28 | ` 29 | .alpha-picker { 30 | position: relative; 31 | } 32 | .color-alpha { 33 | radius: 2px; 34 | } 35 | `, 36 | ], 37 | changeDetection: ChangeDetectionStrategy.OnPush, 38 | preserveWhitespaces: false, 39 | providers: [ 40 | { 41 | provide: NG_VALUE_ACCESSOR, 42 | useExisting: forwardRef(() => AlphaPickerComponent), 43 | multi: true, 44 | }, 45 | { 46 | provide: ColorWrap, 47 | useExisting: forwardRef(() => AlphaPickerComponent), 48 | }, 49 | ], 50 | standalone: false, 51 | }) 52 | export class AlphaPickerComponent extends ColorWrap implements OnChanges { 53 | /** Pixel value for picker width */ 54 | @Input() width: string | number = 316; 55 | /** Pixel value for picker height */ 56 | @Input() height: string | number = 16; 57 | @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; 58 | pointer: { [key: string]: string } = { 59 | width: '18px', 60 | height: '18px', 61 | borderRadius: '50%', 62 | transform: 'translate(-9px, -2px)', 63 | boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.37)', 64 | }; 65 | 66 | constructor() { 67 | super(); 68 | } 69 | ngOnChanges() { 70 | if (this.direction === 'vertical') { 71 | this.pointer.transform = 'translate(-3px, -9px)'; 72 | } 73 | this.setState(toState(this.color, this.oldHue)); 74 | } 75 | handlePickerChange({ data, $event }) { 76 | this.handleChange(data, $event); 77 | } 78 | } 79 | 80 | @NgModule({ 81 | declarations: [AlphaPickerComponent], 82 | exports: [AlphaPickerComponent], 83 | imports: [CommonModule, AlphaModule, CheckboardModule], 84 | }) 85 | export class ColorAlphaModule {} 86 | -------------------------------------------------------------------------------- /src/lib/alpha/alpha.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorAlphaModule } from './alpha-picker.component'; 6 | 7 | export const red = { 8 | hsl: { a: 1, h: 0, l: 0.5, s: 1 }, 9 | hex: '#ff0000', 10 | rgb: { r: 255, g: 0, b: 0, a: 1 }, 11 | hsv: { h: 0, s: 1, v: 1, a: 1 }, 12 | }; 13 | 14 | describe('AlphaComponent', () => { 15 | beforeEach(waitForAsync(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [AlphaTestApp], 18 | imports: [ColorAlphaModule], 19 | }); 20 | 21 | TestBed.compileComponents(); 22 | })); 23 | it(`should apply className to root element`, () => { 24 | const fixture = TestBed.createComponent(AlphaTestApp); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement; 27 | expect(compiled.querySelector('.alpha-picker').className).toContain('classy'); 28 | }); 29 | it(`should draw vertical`, () => { 30 | const fixture = TestBed.createComponent(AlphaTestApp); 31 | const testComponent = fixture.componentInstance; 32 | fixture.detectChanges(); 33 | testComponent.direction = 'vertical'; 34 | fixture.detectChanges(); 35 | const div = fixture.debugElement.query(By.css('.alpha-container')); 36 | expect(div.nativeElement.classList.contains('color-alpha-vertical')).toBe(true); 37 | }); 38 | // it(`should change alpha on mousedown`, () => { 39 | // const fixture = TestBed.createComponent(AlphaPickerComponent); 40 | // const component = fixture.componentInstance; 41 | // component.width = 20; 42 | // component.height = 200; 43 | // component.color = red.hsl; 44 | // fixture.detectChanges(); 45 | // const $event = new MouseEvent('mousedown', { 46 | // bubbles: true, 47 | // cancelable: true, 48 | // view: window, 49 | // clientX: 0, 50 | // clientY: 0, 51 | // }); 52 | // fixture.detectChanges(); 53 | // expect(component.hsl.a).toEqual(0); 54 | // }); 55 | }); 56 | 57 | @Component({ 58 | selector: 'test-app', 59 | template: ``, 63 | standalone: false, 64 | }) 65 | class AlphaTestApp { 66 | className = 'classy'; 67 | direction = 'horizontal'; 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/alpha/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/alpha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/alpha", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/alpha/public_api.ts: -------------------------------------------------------------------------------- 1 | export { AlphaPickerComponent, ColorAlphaModule } from './alpha-picker.component'; 2 | -------------------------------------------------------------------------------- /src/lib/block/block-swatches.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | @Component({ 3 | selector: 'color-block-swatches', 4 | template: ` 5 |
6 | @for (c of colors; track c) { 7 | 14 | } 15 |
16 |
17 | `, 18 | styles: [ 19 | ` 20 | .block-swatches { 21 | margin-right: -10px; 22 | } 23 | .clear { 24 | clear: both; 25 | } 26 | `, 27 | ], 28 | standalone: false, 29 | }) 30 | export class BlockSwatchesComponent { 31 | @Input() colors!: string[]; 32 | @Output() onClick = new EventEmitter(); 33 | @Output() onSwatchHover = new EventEmitter(); 34 | 35 | swatchStyle = { 36 | width: '22px', 37 | height: '22px', 38 | float: 'left', 39 | marginRight: '10px', 40 | marginBottom: '10px', 41 | borderRadius: '4px', 42 | }; 43 | 44 | handleClick({ hex, $event }) { 45 | this.onClick.emit({ hex, $event }); 46 | } 47 | focusStyle(c) { 48 | return { 49 | boxShadow: `${c} 0 0 4px`, 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/block/block.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { 5 | CheckboardModule, 6 | ColorWrap, 7 | EditableInputModule, 8 | getContrastingColor, 9 | isValidHex, 10 | SwatchModule, 11 | } from 'ngx-color'; 12 | import { BlockSwatchesComponent } from './block-swatches.component'; 13 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 14 | 15 | @Component({ 16 | selector: 'color-block', 17 | template: ` 18 |
19 | @if (triangle !== 'hide') { 20 |
24 | } 25 | 26 |
27 | @if (hex === 'transparent') { 28 | 29 | } 30 |
31 | {{ hex }} 32 |
33 |
34 | 35 |
36 | 41 | 46 |
47 |
48 | `, 49 | styles: [ 50 | ` 51 | .block-card { 52 | background: #fff; 53 | border-radius: 6px; 54 | box-shadow: 0 1px rgba(0, 0, 0, 0.1); 55 | position: relative; 56 | } 57 | .block-head { 58 | align-items: center; 59 | border-radius: 6px 6px 0 0; 60 | display: flex; 61 | height: 110px; 62 | justify-content: center; 63 | position: relative; 64 | } 65 | .block-body { 66 | padding: 10px; 67 | } 68 | .block-label { 69 | font-size: 18px; 70 | position: relative; 71 | } 72 | .block-triangle { 73 | border-style: solid; 74 | border-width: 0 10px 10px 10px; 75 | height: 0; 76 | left: 50%; 77 | margin-left: -10px; 78 | position: absolute; 79 | top: -10px; 80 | width: 0; 81 | } 82 | `, 83 | ], 84 | preserveWhitespaces: false, 85 | changeDetection: ChangeDetectionStrategy.OnPush, 86 | providers: [ 87 | { 88 | provide: NG_VALUE_ACCESSOR, 89 | useExisting: forwardRef(() => BlockComponent), 90 | multi: true, 91 | }, 92 | { 93 | provide: ColorWrap, 94 | useExisting: forwardRef(() => BlockComponent), 95 | }, 96 | ], 97 | standalone: false, 98 | }) 99 | export class BlockComponent extends ColorWrap { 100 | /** Pixel value for picker width */ 101 | @Input() width: string | number = 170; 102 | /** Color squares to display */ 103 | @Input() colors = [ 104 | '#D9E3F0', 105 | '#F47373', 106 | '#697689', 107 | '#37D67A', 108 | '#2CCCE4', 109 | '#555555', 110 | '#dce775', 111 | '#ff8a65', 112 | '#ba68c8', 113 | ]; 114 | @Input() triangle: 'top' | 'hide' = 'top'; 115 | input: { [key: string]: string } = { 116 | width: '100%', 117 | fontSize: '12px', 118 | color: '#666', 119 | border: '0px', 120 | outline: 'none', 121 | height: '22px', 122 | boxShadow: 'inset 0 0 0 1px #ddd', 123 | borderRadius: '4px', 124 | padding: '0 7px', 125 | boxSizing: 'border-box', 126 | }; 127 | wrap: { [key: string]: string } = { 128 | position: 'relative', 129 | width: '100%', 130 | }; 131 | disableAlpha = true; 132 | 133 | constructor() { 134 | super(); 135 | } 136 | 137 | handleValueChange({ data, $event }) { 138 | this.handleBlockChange({ hex: data, $event }); 139 | } 140 | getContrastingColor(hex) { 141 | return getContrastingColor(hex); 142 | } 143 | handleBlockChange({ hex, $event }) { 144 | if (isValidHex(hex)) { 145 | // this.hex = hex; 146 | this.handleChange( 147 | { 148 | hex, 149 | source: 'hex', 150 | }, 151 | $event, 152 | ); 153 | } 154 | } 155 | } 156 | 157 | @NgModule({ 158 | declarations: [BlockComponent, BlockSwatchesComponent], 159 | exports: [BlockComponent, BlockSwatchesComponent], 160 | imports: [CommonModule, CheckboardModule, SwatchModule, EditableInputModule], 161 | }) 162 | export class ColorBlockModule {} 163 | -------------------------------------------------------------------------------- /src/lib/block/block.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { BlockComponent, ColorBlockModule } from './block.component'; 6 | 7 | describe('BlockComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [BlockTestApp], 11 | imports: [ColorBlockModule], 12 | }); 13 | 14 | TestBed.compileComponents(); 15 | })); 16 | it(`should apply className to root element`, () => { 17 | const fixture = TestBed.createComponent(BlockTestApp); 18 | fixture.detectChanges(); 19 | const div = fixture.debugElement.query(By.css('.block-card')); 20 | expect(div.nativeElement.classList.contains('classy')).toBe(true); 21 | }); 22 | it(`should change color on swatch click`, () => { 23 | const fixture = TestBed.createComponent(BlockComponent); 24 | const component = fixture.componentInstance; 25 | component.colors = ['#000000']; 26 | fixture.detectChanges(); 27 | const div = fixture.debugElement.query(By.css('.swatch')); 28 | div.triggerEventHandler('click', {}); 29 | fixture.detectChanges(); 30 | expect(component.hex).toEqual('#000000'); 31 | }); 32 | it(`should change color on input`, () => { 33 | const fixture = TestBed.createComponent(BlockComponent); 34 | const component = fixture.componentInstance; 35 | fixture.detectChanges(); 36 | const inputElement = fixture.debugElement.query(By.css('input')); 37 | inputElement.nativeElement.value = '#FFFFFF'; 38 | inputElement.nativeElement.dispatchEvent(new Event('keydown')); 39 | inputElement.nativeElement.dispatchEvent(new Event('keyup')); 40 | fixture.detectChanges(); 41 | expect(component.hex).toEqual('#ffffff'); 42 | }); 43 | }); 44 | 45 | @Component({ 46 | selector: 'test-app', 47 | template: ``, 48 | standalone: false, 49 | }) 50 | class BlockTestApp { 51 | className = 'classy'; 52 | } 53 | -------------------------------------------------------------------------------- /src/lib/block/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/block", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/block/public_api.ts: -------------------------------------------------------------------------------- 1 | export { BlockSwatchesComponent } from './block-swatches.component'; 2 | export { BlockComponent, ColorBlockModule } from './block.component'; 3 | -------------------------------------------------------------------------------- /src/lib/checkboard.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Input, NgModule, OnInit } from '@angular/core'; 3 | 4 | import { getCheckerboard } from './helpers/checkboard'; 5 | 6 | @Component({ 7 | selector: 'color-checkboard', 8 | template: `
`, 9 | styles: [ 10 | ` 11 | .grid { 12 | top: 0px; 13 | right: 0px; 14 | bottom: 0px; 15 | left: 0px; 16 | position: absolute; 17 | } 18 | `, 19 | ], 20 | preserveWhitespaces: false, 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | standalone: false, 23 | }) 24 | export class CheckboardComponent implements OnInit { 25 | @Input() white = 'transparent'; 26 | @Input() size = 8; 27 | @Input() grey = 'rgba(0,0,0,.08)'; 28 | @Input() boxShadow!: string; 29 | @Input() borderRadius!: string; 30 | gridStyles!: Record; 31 | 32 | ngOnInit() { 33 | const background = getCheckerboard(this.white, this.grey, this.size); 34 | this.gridStyles = { 35 | borderRadius: this.borderRadius, 36 | boxShadow: this.boxShadow, 37 | background: `url(${background}) center left`, 38 | }; 39 | } 40 | } 41 | 42 | @NgModule({ 43 | declarations: [CheckboardComponent], 44 | exports: [CheckboardComponent], 45 | imports: [CommonModule], 46 | }) 47 | export class CheckboardModule {} 48 | -------------------------------------------------------------------------------- /src/lib/chrome/chrome-fields.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | } from '@angular/core'; 9 | 10 | import { isValidHex, HSLA, RGBA } from 'ngx-color'; 11 | import { TinyColor } from '@ctrl/tinycolor'; 12 | 13 | @Component({ 14 | selector: 'color-chrome-fields', 15 | template: ` 16 |
17 |
18 | @if (view === 'hex') { 19 |
20 | 26 |
27 | } 28 | @if (view === 'rgb') { 29 |
30 | 36 |
37 |
38 | 44 |
45 |
46 | 52 |
53 |
54 | @if (!disableAlpha) { 55 | 62 | } 63 |
64 | } 65 | @if (view === 'hsl') { 66 |
67 | 73 |
74 |
75 | 81 |
82 |
83 | 89 |
90 |
91 | @if (!disableAlpha) { 92 | 99 | } 100 |
101 | } 102 |
103 | 104 |
105 |
106 | 107 | 112 | 117 | 118 |
119 |
120 |
121 | `, 122 | styles: [ 123 | ` 124 | .chrome-wrap { 125 | padding-top: 16px; 126 | display: flex; 127 | } 128 | .chrome-fields { 129 | flex: 1; 130 | display: flex; 131 | margin-left: -6px; 132 | } 133 | .chrome-field { 134 | padding-left: 6px; 135 | width: 100%; 136 | } 137 | .chrome-toggle { 138 | width: 32px; 139 | text-align: right; 140 | position: relative; 141 | } 142 | .chrome-icon { 143 | margin-right: -4px; 144 | margin-top: 12px; 145 | cursor: pointer; 146 | position: relative; 147 | } 148 | .chrome-toggle-svg { 149 | width: 24px; 150 | height: 24px; 151 | border: 1px transparent solid; 152 | border-radius: 5px; 153 | } 154 | .chrome-toggle-svg:hover { 155 | background: #eee; 156 | } 157 | `, 158 | ], 159 | changeDetection: ChangeDetectionStrategy.OnPush, 160 | preserveWhitespaces: false, 161 | standalone: false, 162 | }) 163 | export class ChromeFieldsComponent implements OnInit { 164 | @Input() disableAlpha!: boolean; 165 | @Input() hsl!: HSLA; 166 | @Input() rgb!: RGBA; 167 | @Input() hex!: string; 168 | @Output() onChange = new EventEmitter(); 169 | view = ''; 170 | input: Record = { 171 | fontSize: '11px', 172 | color: '#333', 173 | width: '100%', 174 | borderRadius: '2px', 175 | border: 'none', 176 | boxShadow: 'inset 0 0 0 1px #dadada', 177 | height: '21px', 178 | 'text-align': 'center', 179 | }; 180 | label: Record = { 181 | 'text-transform': 'uppercase', 182 | fontSize: '11px', 183 | 'line-height': '11px', 184 | color: '#969696', 185 | 'text-align': 'center', 186 | display: 'block', 187 | marginTop: '12px', 188 | }; 189 | 190 | ngOnInit() { 191 | if (this.hsl.a === 1 && this.view !== 'hex') { 192 | this.view = 'hex'; 193 | } else if (this.view !== 'rgb' && this.view !== 'hsl') { 194 | this.view = 'rgb'; 195 | } 196 | } 197 | toggleViews() { 198 | if (this.view === 'hex') { 199 | this.view = 'rgb'; 200 | } else if (this.view === 'rgb') { 201 | this.view = 'hsl'; 202 | } else if (this.view === 'hsl') { 203 | if (this.hsl.a === 1) { 204 | this.view = 'hex'; 205 | } else { 206 | this.view = 'rgb'; 207 | } 208 | } 209 | } 210 | round(value) { 211 | return Math.round(value); 212 | } 213 | handleChange({ data, $event }) { 214 | if (data.hex) { 215 | if (isValidHex(data.hex)) { 216 | const color = new TinyColor(data.hex); 217 | this.onChange.emit({ 218 | data: { 219 | hex: this.disableAlpha ? color.toHex() : color.toHex8(), 220 | source: 'hex', 221 | }, 222 | $event, 223 | }); 224 | } 225 | } else if (data.r || data.g || data.b) { 226 | this.onChange.emit({ 227 | data: { 228 | r: data.r || this.rgb.r, 229 | g: data.g || this.rgb.g, 230 | b: data.b || this.rgb.b, 231 | source: 'rgb', 232 | }, 233 | $event, 234 | }); 235 | } else if (data.a) { 236 | if (data.a < 0) { 237 | data.a = 0; 238 | } else if (data.a > 1) { 239 | data.a = 1; 240 | } 241 | 242 | if (this.disableAlpha) { 243 | data.a = 1; 244 | } 245 | 246 | this.onChange.emit({ 247 | data: { 248 | h: this.hsl.h, 249 | s: this.hsl.s, 250 | l: this.hsl.l, 251 | a: Math.round(data.a * 100) / 100, 252 | source: 'rgb', 253 | }, 254 | $event, 255 | }); 256 | } else if (data.h || data.s || data.l) { 257 | const s = data.s && data.s.replace('%', ''); 258 | const l = data.l && data.l.replace('%', ''); 259 | this.onChange.emit({ 260 | data: { 261 | h: data.h || this.hsl.h, 262 | s: Number(s || this.hsl.s), 263 | l: Number(l || this.hsl.l), 264 | source: 'hsl', 265 | }, 266 | $event, 267 | }); 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/lib/chrome/chrome.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { 5 | AlphaModule, 6 | CheckboardModule, 7 | ColorWrap, 8 | EditableInputModule, 9 | HueModule, 10 | SaturationModule, 11 | } from 'ngx-color'; 12 | import { ChromeFieldsComponent } from './chrome-fields.component'; 13 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 14 | 15 | @Component({ 16 | selector: 'color-chrome', 17 | template: ` 18 |
19 |
20 | 26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | 43 |
44 | @if (!disableAlpha) { 45 |
46 | 53 |
54 | } 55 |
56 |
57 | 64 |
65 |
66 | `, 67 | styles: [ 68 | ` 69 | .chrome-picker { 70 | background: #fff; 71 | border-radius: 2px; 72 | box-shadow: 73 | 0 0 2px rgba(0, 0, 0, 0.3), 74 | 0 4px 8px rgba(0, 0, 0, 0.3); 75 | box-sizing: initial; 76 | width: 225px; 77 | font-family: 'Menlo'; 78 | } 79 | .chrome-controls { 80 | display: flex; 81 | } 82 | .chrome-color { 83 | width: 42px; 84 | } 85 | .chrome-body { 86 | padding: 14px 14px 12px; 87 | } 88 | .chrome-active { 89 | position: absolute; 90 | top: 0; 91 | bottom: 0; 92 | left: 0; 93 | right: 0; 94 | border-radius: 20px; 95 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 96 | z-index: 2; 97 | } 98 | .chrome-swatch { 99 | width: 28px; 100 | height: 28px; 101 | border-radius: 15px; 102 | position: relative; 103 | overflow: hidden; 104 | } 105 | .saturation { 106 | width: 100%; 107 | padding-bottom: 55%; 108 | position: relative; 109 | border-radius: 2px 2px 0 0; 110 | overflow: hidden; 111 | } 112 | .chrome-toggles { 113 | flex: 1; 114 | } 115 | .chrome-hue { 116 | height: 10px; 117 | position: relative; 118 | margin-bottom: 8px; 119 | } 120 | .chrome-alpha { 121 | height: 10px; 122 | position: relative; 123 | } 124 | `, 125 | ], 126 | changeDetection: ChangeDetectionStrategy.OnPush, 127 | preserveWhitespaces: false, 128 | providers: [ 129 | { 130 | provide: NG_VALUE_ACCESSOR, 131 | useExisting: forwardRef(() => ChromeComponent), 132 | multi: true, 133 | }, 134 | { 135 | provide: ColorWrap, 136 | useExisting: forwardRef(() => ChromeComponent), 137 | }, 138 | ], 139 | standalone: false, 140 | }) 141 | export class ChromeComponent extends ColorWrap { 142 | /** Remove alpha slider and options from picker */ 143 | @Input() disableAlpha = false; 144 | circle: Record = { 145 | width: '12px', 146 | height: '12px', 147 | borderRadius: '6px', 148 | boxShadow: 'rgb(255, 255, 255) 0px 0px 0px 1px inset', 149 | transform: 'translate(-6px, -8px)', 150 | }; 151 | pointer: Record = { 152 | width: '12px', 153 | height: '12px', 154 | borderRadius: '6px', 155 | transform: 'translate(-6px, -2px)', 156 | backgroundColor: 'rgb(248, 248, 248)', 157 | boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.37)', 158 | }; 159 | activeBackground!: string; 160 | 161 | constructor() { 162 | super(); 163 | } 164 | 165 | afterValidChange() { 166 | const alpha = this.disableAlpha ? 1 : this.rgb.a; 167 | this.activeBackground = `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, ${alpha})`; 168 | } 169 | handleValueChange({ data, $event }) { 170 | this.handleChange(data, $event); 171 | } 172 | } 173 | 174 | @NgModule({ 175 | declarations: [ChromeComponent, ChromeFieldsComponent], 176 | exports: [ChromeComponent, ChromeFieldsComponent], 177 | imports: [ 178 | CommonModule, 179 | AlphaModule, 180 | CheckboardModule, 181 | EditableInputModule, 182 | HueModule, 183 | SaturationModule, 184 | ], 185 | }) 186 | export class ColorChromeModule {} 187 | -------------------------------------------------------------------------------- /src/lib/chrome/chrome.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorChromeModule } from './chrome.component'; 6 | 7 | describe('BlockComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ChromeTestApp], 11 | imports: [ColorChromeModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(ChromeTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.chrome-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ``, 25 | standalone: false, 26 | }) 27 | class ChromeTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/chrome/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/chrome", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/chrome/public_api.ts: -------------------------------------------------------------------------------- 1 | export { ChromeFieldsComponent } from './chrome-fields.component'; 2 | export { ChromeComponent, ColorChromeModule } from './chrome.component'; 3 | -------------------------------------------------------------------------------- /src/lib/circle/circle-swatch.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnChanges, 7 | Output, 8 | } from '@angular/core'; 9 | 10 | @Component({ 11 | selector: 'color-circle-swatch', 12 | template: ` 13 |
20 | 28 | 29 |
30 |
31 | `, 32 | styles: [ 33 | ` 34 | .circle-swatch { 35 | transform: scale(1); 36 | transition: transform 100ms ease; 37 | } 38 | .circle-swatch:hover { 39 | transform: scale(1.2); 40 | } 41 | `, 42 | ], 43 | changeDetection: ChangeDetectionStrategy.OnPush, 44 | preserveWhitespaces: false, 45 | standalone: false, 46 | }) 47 | export class CircleSwatchComponent implements OnChanges { 48 | @Input() color!: string; 49 | @Input() circleSize = 28; 50 | @Input() circleSpacing = 14; 51 | @Input() focus = false; 52 | @Output() onClick = new EventEmitter(); 53 | @Output() onSwatchHover = new EventEmitter(); 54 | focusStyle: Record = {}; 55 | swatchStyle: Record = { 56 | borderRadius: '50%', 57 | background: 'transparent', 58 | transition: '100ms box-shadow ease 0s', 59 | }; 60 | 61 | ngOnChanges() { 62 | this.swatchStyle.boxShadow = `inset 0 0 0 ${this.circleSize / 2}px ${this.color}`; 63 | this.focusStyle.boxShadow = `inset 0 0 0 ${this.circleSize / 2}px ${this.color}, 0 0 5px ${this.color}`; 64 | if (this.focus) { 65 | this.focusStyle.boxShadow = `inset 0 0 0 3px ${this.color}, 0 0 5px ${this.color}`; 66 | } 67 | } 68 | handleClick({ hex, $event }) { 69 | this.onClick.emit({ hex, $event }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/circle/circle.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | import { 4 | amber, 5 | blue, 6 | blueGrey, 7 | brown, 8 | cyan, 9 | deepOrange, 10 | deepPurple, 11 | green, 12 | indigo, 13 | lightBlue, 14 | lightGreen, 15 | lime, 16 | orange, 17 | pink, 18 | purple, 19 | red, 20 | teal, 21 | yellow, 22 | } from 'material-colors'; 23 | import { TinyColor } from '@ctrl/tinycolor'; 24 | 25 | import { ColorWrap, isValidHex, SwatchModule } from 'ngx-color'; 26 | import { CircleSwatchComponent } from './circle-swatch.component'; 27 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 28 | 29 | @Component({ 30 | selector: 'color-circle', 31 | template: ` 32 |
38 | @for (color of colors; track color) { 39 | 47 | } 48 |
49 | `, 50 | styles: [ 51 | ` 52 | .circle-picker { 53 | display: flex; 54 | flex-wrap: wrap; 55 | } 56 | `, 57 | ], 58 | changeDetection: ChangeDetectionStrategy.OnPush, 59 | preserveWhitespaces: false, 60 | providers: [ 61 | { 62 | provide: NG_VALUE_ACCESSOR, 63 | useExisting: forwardRef(() => CircleComponent), 64 | multi: true, 65 | }, 66 | { 67 | provide: ColorWrap, 68 | useExisting: forwardRef(() => CircleComponent), 69 | }, 70 | ], 71 | standalone: false, 72 | }) 73 | export class CircleComponent extends ColorWrap { 74 | /** Pixel value for picker width */ 75 | @Input() width: string | number = 252; 76 | /** Color squares to display */ 77 | @Input() 78 | colors: string[] = [ 79 | red['500'], 80 | pink['500'], 81 | purple['500'], 82 | deepPurple['500'], 83 | indigo['500'], 84 | blue['500'], 85 | lightBlue['500'], 86 | cyan['500'], 87 | teal['500'], 88 | green['500'], 89 | lightGreen['500'], 90 | lime['500'], 91 | yellow['500'], 92 | amber['500'], 93 | orange['500'], 94 | deepOrange['500'], 95 | brown['500'], 96 | blueGrey['500'], 97 | ]; 98 | /** Value for circle size */ 99 | @Input() circleSize = 28; 100 | /** Value for spacing between circles */ 101 | @Input() circleSpacing = 14; 102 | 103 | constructor() { 104 | super(); 105 | } 106 | isActive(color: string) { 107 | return new TinyColor(this.hex).equals(color); 108 | } 109 | handleBlockChange({ hex, $event }: { hex: string; $event: Event }) { 110 | if (isValidHex(hex)) { 111 | this.handleChange({ hex, source: 'hex' }, $event); 112 | } 113 | } 114 | handleValueChange({ data, $event }) { 115 | this.handleChange(data, $event); 116 | } 117 | } 118 | 119 | @NgModule({ 120 | declarations: [CircleComponent, CircleSwatchComponent], 121 | exports: [CircleComponent, CircleSwatchComponent], 122 | imports: [CommonModule, SwatchModule], 123 | }) 124 | export class ColorCircleModule {} 125 | -------------------------------------------------------------------------------- /src/lib/circle/circle.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorCircleModule } from './circle.component'; 6 | 7 | describe('BlockComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [CircleTestApp], 11 | imports: [ColorCircleModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(CircleTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.circle-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ``, 25 | standalone: false, 26 | }) 27 | class CircleTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/circle/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/circle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/circle", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT", 9 | "dependencies": { 10 | "material-colors": "^1.2.6" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/circle/public_api.ts: -------------------------------------------------------------------------------- 1 | export { CircleSwatchComponent } from './circle-swatch.component'; 2 | export { CircleComponent, ColorCircleModule } from './circle.component'; 3 | -------------------------------------------------------------------------------- /src/lib/color-wrap.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | Component, 4 | EventEmitter, 5 | forwardRef, 6 | Input, 7 | isDevMode, 8 | NgModule, 9 | OnChanges, 10 | OnDestroy, 11 | OnInit, 12 | Output, 13 | } from '@angular/core'; 14 | 15 | import { Subscription } from 'rxjs'; 16 | import { debounceTime, tap } from 'rxjs/operators'; 17 | 18 | import { simpleCheckForValidColor, toState } from './helpers/color'; 19 | import { Color, HSLA, HSVA, RGBA } from './helpers/color.interfaces'; 20 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 21 | 22 | export interface ColorEvent { 23 | $event: Event; 24 | color: Color; 25 | } 26 | 27 | export enum ColorMode { 28 | HEX = 'hex', 29 | HSL = 'hsl', 30 | HSV = 'hsv', 31 | RGB = 'rgb', 32 | } 33 | 34 | @Component({ 35 | // create seletor base for test override property 36 | selector: 'color-wrap', 37 | template: ``, 38 | providers: [ 39 | { 40 | provide: NG_VALUE_ACCESSOR, 41 | useExisting: forwardRef(() => ColorWrap), 42 | multi: true, 43 | }, 44 | ], 45 | standalone: false, 46 | }) 47 | export class ColorWrap implements OnInit, OnChanges, OnDestroy, ControlValueAccessor { 48 | @Input() className?: string; 49 | 50 | /** 51 | * Descriptors the return color format if the component is used with two-way binding 52 | */ 53 | @Input() mode: ColorMode = ColorMode.HEX; 54 | 55 | @Input() color: HSLA | HSVA | RGBA | string = { 56 | h: 250, 57 | s: 0.5, 58 | l: 0.2, 59 | a: 1, 60 | }; 61 | @Output() colorChange = new EventEmitter(); 62 | @Output() onChange = new EventEmitter(); 63 | @Output() onChangeComplete = new EventEmitter(); 64 | @Output() onSwatchHover = new EventEmitter(); 65 | oldHue!: number; 66 | hsl!: HSLA; 67 | hsv!: HSVA; 68 | rgb!: RGBA; 69 | hex!: string; 70 | source!: string; 71 | currentColor!: string; 72 | changes?: Subscription; 73 | disableAlpha?: boolean; 74 | 75 | private _onChangeCompleteSubscription = new Subscription(); 76 | private _onSwatchHoverSubscription = new Subscription(); 77 | 78 | ngOnInit() { 79 | this.changes = this.onChange 80 | .pipe( 81 | debounceTime(100), 82 | tap(event => { 83 | this.onChangeComplete.emit(event); 84 | switch (this.mode) { 85 | case ColorMode.HEX: 86 | this.colorChange.emit(event.color.hex); 87 | break; 88 | case ColorMode.HSL: 89 | this.colorChange.emit(event.color.hsl); 90 | break; 91 | case ColorMode.HSV: 92 | this.colorChange.emit(event.color.hsv); 93 | break; 94 | case ColorMode.RGB: 95 | this.colorChange.emit(event.color.rgb); 96 | break; 97 | default: 98 | const msg = `The mode '${this.mode}' is not supported`; 99 | if (isDevMode()) { 100 | throw new Error(msg); 101 | } else { 102 | console.warn(msg); 103 | } 104 | break; 105 | } 106 | }), 107 | ) 108 | .subscribe(); 109 | this.setState(toState(this.color, 0)); 110 | this.currentColor = this.hex; 111 | } 112 | ngOnChanges() { 113 | this.setState(toState(this.color, this.oldHue)); 114 | } 115 | ngOnDestroy() { 116 | this.changes?.unsubscribe(); 117 | this._onChangeCompleteSubscription?.unsubscribe(); 118 | this._onSwatchHoverSubscription?.unsubscribe(); 119 | } 120 | setState(data) { 121 | this.oldHue = data.oldHue; 122 | this.hsl = data.hsl; 123 | this.hsv = data.hsv; 124 | this.rgb = data.rgb; 125 | this.hex = data.hex; 126 | this.source = data.source; 127 | this.afterValidChange(); 128 | } 129 | handleChange(data, $event) { 130 | const isValidColor = simpleCheckForValidColor(data); 131 | if (isValidColor) { 132 | const color = toState(data, data.h || this.oldHue, this.disableAlpha); 133 | this.setState(color); 134 | this.onChange.emit({ color, $event }); 135 | this.afterValidChange(); 136 | } 137 | } 138 | /** hook for components after a complete change */ 139 | afterValidChange() {} 140 | 141 | handleSwatchHover(data, $event) { 142 | const isValidColor = simpleCheckForValidColor(data); 143 | if (isValidColor) { 144 | const color = toState(data, data.h || this.oldHue); 145 | this.setState(color); 146 | this.onSwatchHover.emit({ color, $event }); 147 | } 148 | } 149 | 150 | registerOnChange(fn: (hex: string) => void): void { 151 | this._onChangeCompleteSubscription.add( 152 | this.onChangeComplete.pipe(tap(event => fn(event.color.hex))).subscribe(), 153 | ); 154 | } 155 | 156 | registerOnTouched(fn: () => void): void { 157 | this._onSwatchHoverSubscription.add(this.onSwatchHover.pipe(tap(() => fn())).subscribe()); 158 | } 159 | 160 | setDisabledState(isDisabled: boolean): void {} 161 | 162 | writeValue(hex: string): void { 163 | this.color = hex; 164 | } 165 | } 166 | 167 | @NgModule({ 168 | declarations: [ColorWrap], 169 | exports: [ColorWrap], 170 | imports: [CommonModule], 171 | }) 172 | export class ColorWrapModule {} 173 | -------------------------------------------------------------------------------- /src/lib/compact/compact-color.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnChanges, 7 | Output, 8 | } from '@angular/core'; 9 | 10 | import { getContrastingColor } from 'ngx-color'; 11 | 12 | @Component({ 13 | selector: 'color-compact-color', 14 | template: ` 15 |
16 | 24 |
29 |
30 |
31 | `, 32 | styles: [ 33 | ` 34 | .compact-dot { 35 | position: absolute; 36 | top: 5px; 37 | right: 5px; 38 | bottom: 5px; 39 | left: 5px; 40 | border-radius: 50%; 41 | opacity: 0; 42 | } 43 | .compact-dot.active { 44 | opacity: 1; 45 | } 46 | `, 47 | ], 48 | changeDetection: ChangeDetectionStrategy.OnPush, 49 | preserveWhitespaces: false, 50 | standalone: false, 51 | }) 52 | export class CompactColorComponent implements OnChanges { 53 | @Input() color!: string; 54 | @Input() active!: boolean; 55 | @Output() onClick = new EventEmitter(); 56 | @Output() onSwatchHover = new EventEmitter(); 57 | swatchStyle: Record = { 58 | width: '15px', 59 | height: '15px', 60 | float: 'left', 61 | marginRight: '5px', 62 | marginBottom: '5px', 63 | position: 'relative', 64 | cursor: 'pointer', 65 | }; 66 | swatchFocus: Record = {}; 67 | getContrastingColor = getContrastingColor; 68 | 69 | ngOnChanges() { 70 | this.swatchStyle.background = this.color; 71 | this.swatchFocus.boxShadow = `0 0 4px ${this.color}`; 72 | if (this.color.toLowerCase() === '#ffffff') { 73 | this.swatchStyle.boxShadow = 'inset 0 0 0 1px #ddd'; 74 | } 75 | } 76 | handleClick({ hex, $event }) { 77 | this.onClick.emit({ hex, $event }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/compact/compact-fields.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { isValidHex, RGBA } from 'ngx-color'; 4 | 5 | @Component({ 6 | selector: 'color-compact-fields', 7 | template: ` 8 |
9 |
10 |
11 | 17 |
18 |
19 | 25 |
26 |
27 | 33 |
34 |
35 | 41 |
42 |
43 | `, 44 | styles: [ 45 | ` 46 | .compact-fields { 47 | display: flex; 48 | padding-bottom: 6px; 49 | padding-right: 5px; 50 | position: relative; 51 | } 52 | .compact-active { 53 | position: absolute; 54 | top: 6px; 55 | left: 5px; 56 | height: 9px; 57 | width: 9px; 58 | } 59 | `, 60 | ], 61 | changeDetection: ChangeDetectionStrategy.OnPush, 62 | preserveWhitespaces: false, 63 | standalone: false, 64 | }) 65 | export class CompactFieldsComponent { 66 | @Input() hex!: string; 67 | @Input() rgb!: RGBA; 68 | @Output() onChange = new EventEmitter(); 69 | HEXWrap: { [key: string]: string } = { 70 | marginTop: '-3px', 71 | marginBottom: '-3px', 72 | // flex: '6 1 0%', 73 | position: 'relative', 74 | }; 75 | HEXinput: { [key: string]: string } = { 76 | width: '80%', 77 | padding: '0px', 78 | paddingLeft: '20%', 79 | border: 'none', 80 | outline: 'none', 81 | background: 'none', 82 | fontSize: '12px', 83 | color: '#333', 84 | height: '16px', 85 | }; 86 | HEXlabel: { [key: string]: string } = { 87 | display: 'none', 88 | }; 89 | RGBwrap: { [key: string]: string } = { 90 | marginTop: '-3px', 91 | marginBottom: '-3px', 92 | // flex: '3 1 0%', 93 | position: 'relative', 94 | }; 95 | RGBinput: { [key: string]: string } = { 96 | width: '80%', 97 | padding: '0px', 98 | paddingLeft: '30%', 99 | border: 'none', 100 | outline: 'none', 101 | background: 'none', 102 | fontSize: '12px', 103 | color: '#333', 104 | height: '16px', 105 | }; 106 | RGBlabel: { [key: string]: string } = { 107 | position: 'absolute', 108 | top: '6px', 109 | left: '0px', 110 | 'line-height': '16px', 111 | 'text-transform': 'uppercase', 112 | fontSize: '12px', 113 | color: '#999', 114 | }; 115 | 116 | handleChange({ data, $event }) { 117 | if (data.hex) { 118 | if (isValidHex(data.hex)) { 119 | this.onChange.emit({ 120 | data: { 121 | hex: data.hex, 122 | source: 'hex', 123 | }, 124 | $event, 125 | }); 126 | } 127 | } else { 128 | this.onChange.emit({ 129 | data: { 130 | r: data.r || this.rgb.r, 131 | g: data.g || this.rgb.g, 132 | b: data.b || this.rgb.b, 133 | source: 'rgb', 134 | }, 135 | $event, 136 | }); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/lib/compact/compact.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { 5 | ColorWrap, 6 | EditableInputModule, 7 | isValidHex, 8 | RaisedModule, 9 | SwatchModule, 10 | zDepth, 11 | } from 'ngx-color'; 12 | import { CompactColorComponent } from './compact-color.component'; 13 | import { CompactFieldsComponent } from './compact-fields.component'; 14 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 15 | 16 | @Component({ 17 | selector: 'color-compact', 18 | template: ` 19 | 25 |
26 |
27 | @for (color of colors; track color) { 28 | 33 | } 34 |
35 |
36 | 41 |
42 |
43 | `, 44 | styles: [ 45 | ` 46 | .color-compact { 47 | background: #f6f6f6; 48 | radius: 4px; 49 | } 50 | .compact-picker { 51 | padding-top: 5px; 52 | padding-left: 5px; 53 | box-sizing: border-box; 54 | width: 245px; 55 | } 56 | .compact-clear { 57 | clear: both; 58 | } 59 | `, 60 | ], 61 | changeDetection: ChangeDetectionStrategy.OnPush, 62 | preserveWhitespaces: false, 63 | providers: [ 64 | { 65 | provide: NG_VALUE_ACCESSOR, 66 | useExisting: forwardRef(() => CompactComponent), 67 | multi: true, 68 | }, 69 | { 70 | provide: ColorWrap, 71 | useExisting: forwardRef(() => CompactComponent), 72 | }, 73 | ], 74 | standalone: false, 75 | }) 76 | export class CompactComponent extends ColorWrap { 77 | /** Color squares to display */ 78 | @Input() colors = [ 79 | '#4D4D4D', 80 | '#999999', 81 | '#FFFFFF', 82 | '#F44E3B', 83 | '#FE9200', 84 | '#FCDC00', 85 | '#DBDF00', 86 | '#A4DD00', 87 | '#68CCCA', 88 | '#73D8FF', 89 | '#AEA1FF', 90 | '#FDA1FF', 91 | '#333333', 92 | '#808080', 93 | '#cccccc', 94 | '#D33115', 95 | '#E27300', 96 | '#FCC400', 97 | '#B0BC00', 98 | '#68BC00', 99 | '#16A5A5', 100 | '#009CE0', 101 | '#7B64FF', 102 | '#FA28FF', 103 | '#000000', 104 | '#666666', 105 | '#B3B3B3', 106 | '#9F0500', 107 | '#C45100', 108 | '#FB9E00', 109 | '#808900', 110 | '#194D33', 111 | '#0C797D', 112 | '#0062B1', 113 | '#653294', 114 | '#AB149E', 115 | ]; 116 | @Input() zDepth: zDepth = 1; 117 | @Input() radius = 1; 118 | @Input() background = '#fff'; 119 | disableAlpha = true; 120 | 121 | constructor() { 122 | super(); 123 | } 124 | handleBlockChange({ hex, $event }) { 125 | if (isValidHex(hex)) { 126 | this.handleChange({ hex, source: 'hex' }, $event); 127 | } 128 | } 129 | handleValueChange({ data, $event }) { 130 | this.handleChange(data, $event); 131 | } 132 | } 133 | 134 | @NgModule({ 135 | declarations: [CompactComponent, CompactColorComponent, CompactFieldsComponent], 136 | exports: [CompactComponent, CompactColorComponent, CompactFieldsComponent], 137 | imports: [CommonModule, EditableInputModule, SwatchModule, RaisedModule], 138 | }) 139 | export class ColorCompactModule {} 140 | -------------------------------------------------------------------------------- /src/lib/compact/compact.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorCompactModule } from './compact.component'; 6 | 7 | describe('CompactComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [CompactTestApp], 11 | imports: [ColorCompactModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(CompactTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.compact-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ``, 25 | standalone: false, 26 | }) 27 | class CompactTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/compact/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/compact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/compact", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/compact/public_api.ts: -------------------------------------------------------------------------------- 1 | export { CompactColorComponent } from './compact-color.component'; 2 | export { CompactFieldsComponent } from './compact-fields.component'; 3 | export { ColorCompactModule, CompactComponent } from './compact.component'; 4 | -------------------------------------------------------------------------------- /src/lib/coordinates.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | HostListener, 5 | NgModule, 6 | OnDestroy, 7 | OnInit, 8 | Output, 9 | inject, 10 | } from '@angular/core'; 11 | 12 | import { Subject, Subscription } from 'rxjs'; 13 | import { distinctUntilChanged } from 'rxjs/operators'; 14 | 15 | @Directive({ 16 | selector: '[ngx-color-coordinates]', 17 | standalone: false, 18 | }) 19 | export class CoordinatesDirective implements OnInit, OnDestroy { 20 | private el = inject(ElementRef); 21 | 22 | @Output() 23 | coordinatesChange = new Subject<{ 24 | x: number; 25 | y: number; 26 | top: number; 27 | left: number; 28 | containerWidth: number; 29 | containerHeight: number; 30 | $event: any; 31 | }>(); 32 | private mousechange = new Subject<{ 33 | x: number; 34 | y: number; 35 | $event: any; 36 | isTouch: boolean; 37 | }>(); 38 | 39 | private mouseListening = false; 40 | private sub?: Subscription; 41 | @HostListener('window:mousemove', ['$event', '$event.pageX', '$event.pageY']) 42 | @HostListener('window:touchmove', [ 43 | '$event', 44 | '$event.touches[0].clientX', 45 | '$event.touches[0].clientY', 46 | 'true', 47 | ]) 48 | mousemove($event: Event, x: number, y: number, isTouch = false) { 49 | if (this.mouseListening) { 50 | $event.preventDefault(); 51 | this.mousechange.next({ $event, x, y, isTouch }); 52 | } 53 | } 54 | @HostListener('window:mouseup') 55 | @HostListener('window:touchend') 56 | mouseup() { 57 | this.mouseListening = false; 58 | } 59 | @HostListener('mousedown', ['$event', '$event.pageX', '$event.pageY']) 60 | @HostListener('touchstart', [ 61 | '$event', 62 | '$event.touches[0].clientX', 63 | '$event.touches[0].clientY', 64 | 'true', 65 | ]) 66 | mousedown($event: Event, x: number, y: number, isTouch = false) { 67 | $event.preventDefault(); 68 | this.mouseListening = true; 69 | this.mousechange.next({ $event, x, y, isTouch }); 70 | } 71 | 72 | ngOnInit() { 73 | this.sub = this.mousechange 74 | .pipe( 75 | // limit times it is updated for the same area 76 | distinctUntilChanged((p, q) => p.x === q.x && p.y === q.y), 77 | ) 78 | .subscribe(n => this.handleChange(n.x, n.y, n.$event, n.isTouch)); 79 | } 80 | 81 | ngOnDestroy() { 82 | this.sub?.unsubscribe(); 83 | } 84 | 85 | handleChange(x: number, y: number, $event: Event, isTouch: boolean) { 86 | const containerWidth = this.el.nativeElement.clientWidth; 87 | const containerHeight = this.el.nativeElement.clientHeight; 88 | const left = x - (this.el.nativeElement.getBoundingClientRect().left + window.pageXOffset); 89 | let top = y - this.el.nativeElement.getBoundingClientRect().top; 90 | 91 | if (!isTouch) { 92 | top = top - window.pageYOffset; 93 | } 94 | this.coordinatesChange.next({ 95 | x, 96 | y, 97 | top, 98 | left, 99 | containerWidth, 100 | containerHeight, 101 | $event, 102 | }); 103 | } 104 | } 105 | 106 | @NgModule({ 107 | declarations: [CoordinatesDirective], 108 | exports: [CoordinatesDirective], 109 | }) 110 | export class CoordinatesModule {} 111 | -------------------------------------------------------------------------------- /src/lib/editable-input.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | OnDestroy, 10 | OnInit, 11 | Output, 12 | } from '@angular/core'; 13 | 14 | import { fromEvent, Subscription } from 'rxjs'; 15 | 16 | let nextUniqueId = 0; 17 | 18 | @Component({ 19 | selector: 'color-editable-input', 20 | template: ` 21 |
22 | 33 | @if (label) { 34 | 35 | {{ label }} 36 | 37 | } 38 |
39 | `, 40 | styles: [ 41 | ` 42 | :host { 43 | display: flex; 44 | } 45 | .wrap { 46 | position: relative; 47 | } 48 | `, 49 | ], 50 | changeDetection: ChangeDetectionStrategy.OnPush, 51 | standalone: false, 52 | }) 53 | export class EditableInputComponent implements OnInit, OnChanges, OnDestroy { 54 | @Input() style!: { 55 | wrap?: Record; 56 | input?: Record; 57 | label?: Record; 58 | }; 59 | @Input() label!: string; 60 | @Input() value!: string | number; 61 | @Input() arrowOffset!: number; 62 | @Input() dragLabel!: boolean; 63 | @Input() dragMax!: number; 64 | @Input() placeholder = ''; 65 | @Output() onChange = new EventEmitter(); 66 | currentValue!: string | number; 67 | blurValue!: string; 68 | wrapStyle!: Record; 69 | inputStyle!: Record; 70 | labelStyle!: Record; 71 | focus = false; 72 | mousemove!: Subscription; 73 | mouseup!: Subscription; 74 | uniqueId: string = `editableInput-${++nextUniqueId}`; 75 | 76 | ngOnInit() { 77 | this.wrapStyle = this.style && this.style.wrap ? this.style.wrap : {}; 78 | this.inputStyle = this.style && this.style.input ? this.style.input : {}; 79 | this.labelStyle = this.style && this.style.label ? this.style.label : {}; 80 | if (this.dragLabel) { 81 | this.labelStyle.cursor = 'ew-resize'; 82 | } 83 | } 84 | handleFocus($event) { 85 | this.focus = true; 86 | } 87 | handleFocusOut($event) { 88 | this.focus = false; 89 | this.currentValue = this.blurValue; 90 | } 91 | handleKeydown($event) { 92 | // In case `e.target.value` is a percentage remove the `%` character 93 | // and update accordingly with a percentage 94 | // https://github.com/casesandberg/react-color/issues/383 95 | const stringValue = String($event.target.value); 96 | const isPercentage = stringValue.indexOf('%') > -1; 97 | const num = Number(stringValue.replace(/%/g, '')); 98 | if (isNaN(num)) { 99 | return; 100 | } 101 | const amount = this.arrowOffset || 1; 102 | 103 | // Up 104 | if ($event.keyCode === 38) { 105 | if (this.label) { 106 | this.onChange.emit({ 107 | data: { [this.label]: num + amount }, 108 | $event, 109 | }); 110 | } else { 111 | this.onChange.emit({ data: num + amount, $event }); 112 | } 113 | 114 | if (isPercentage) { 115 | this.currentValue = `${num + amount}%`; 116 | } else { 117 | this.currentValue = num + amount; 118 | } 119 | } 120 | 121 | // Down 122 | if ($event.keyCode === 40) { 123 | if (this.label) { 124 | this.onChange.emit({ 125 | data: { [this.label]: num - amount }, 126 | $event, 127 | }); 128 | } else { 129 | this.onChange.emit({ data: num - amount, $event }); 130 | } 131 | 132 | if (isPercentage) { 133 | this.currentValue = `${num - amount}%`; 134 | } else { 135 | this.currentValue = num - amount; 136 | } 137 | } 138 | } 139 | handleKeyup($event) { 140 | if ($event.keyCode === 40 || $event.keyCode === 38) { 141 | return; 142 | } 143 | if (`${this.currentValue}` === $event.target.value) { 144 | return; 145 | } 146 | 147 | if (this.label) { 148 | this.onChange.emit({ 149 | data: { [this.label]: $event.target.value }, 150 | $event, 151 | }); 152 | } else { 153 | this.onChange.emit({ data: $event.target.value, $event }); 154 | } 155 | } 156 | ngOnChanges() { 157 | if (!this.focus) { 158 | this.currentValue = String(this.value).toUpperCase(); 159 | this.blurValue = String(this.value).toUpperCase(); 160 | } else { 161 | this.blurValue = String(this.value).toUpperCase(); 162 | } 163 | } 164 | ngOnDestroy() { 165 | this.unsubscribe(); 166 | } 167 | subscribe() { 168 | this.mousemove = fromEvent(document, 'mousemove').subscribe((ev: Event) => this.handleDrag(ev)); 169 | this.mouseup = fromEvent(document, 'mouseup').subscribe(() => this.unsubscribe()); 170 | } 171 | unsubscribe() { 172 | this.mousemove?.unsubscribe(); 173 | this.mouseup?.unsubscribe(); 174 | } 175 | handleMousedown($event: Event) { 176 | if (this.dragLabel) { 177 | $event.preventDefault(); 178 | this.handleDrag($event); 179 | this.subscribe(); 180 | } 181 | } 182 | handleDrag($event) { 183 | if (this.dragLabel) { 184 | const newValue = Math.round(this.value + $event.movementX); 185 | if (newValue >= 0 && newValue <= this.dragMax) { 186 | this.onChange.emit({ data: { [this.label]: newValue }, $event }); 187 | } 188 | } 189 | } 190 | } 191 | 192 | @NgModule({ 193 | declarations: [EditableInputComponent], 194 | exports: [EditableInputComponent], 195 | imports: [CommonModule], 196 | }) 197 | export class EditableInputModule {} 198 | -------------------------------------------------------------------------------- /src/lib/github/github-swatch.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'color-github-swatch', 5 | template: ` 6 |
7 | 14 |
15 |
16 | `, 17 | styles: [ 18 | ` 19 | .github-swatch { 20 | width: 25px; 21 | height: 25px; 22 | font-size: 0; 23 | } 24 | `, 25 | ], 26 | changeDetection: ChangeDetectionStrategy.OnPush, 27 | preserveWhitespaces: false, 28 | standalone: false, 29 | }) 30 | export class GithubSwatchComponent { 31 | @Input() color!: string; 32 | @Output() onClick = new EventEmitter(); 33 | @Output() onSwatchHover = new EventEmitter(); 34 | focusStyle = { 35 | position: 'relative', 36 | 'z-index': '2', 37 | outline: '2px solid #fff', 38 | 'box-shadow': '0 0 5px 2px rgba(0,0,0,0.25)', 39 | }; 40 | 41 | handleClick({ hex, $event }) { 42 | this.onClick.emit({ hex, $event }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/github/github.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { ColorWrap, isValidHex, SwatchModule } from 'ngx-color'; 5 | import { GithubSwatchComponent } from './github-swatch.component'; 6 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 7 | 8 | @Component({ 9 | selector: 'color-github', 10 | template: ` 11 |
12 |
13 |
14 | @for (color of colors; track color) { 15 | 20 | } 21 |
22 | `, 23 | styles: [ 24 | ` 25 | .github-picker { 26 | background: rgb(255, 255, 255); 27 | border: 1px solid rgba(0, 0, 0, 0.2); 28 | box-shadow: rgba(0, 0, 0, 0.15) 0px 3px 12px; 29 | border-radius: 4px; 30 | position: relative; 31 | padding: 5px; 32 | display: flex; 33 | flex-wrap: wrap; 34 | box-sizing: border-box; 35 | } 36 | .triangleShadow { 37 | position: absolute; 38 | border-width: 8px; 39 | border-style: solid; 40 | border-color: transparent transparent rgba(0, 0, 0, 0.15); 41 | border-image: initial; 42 | } 43 | .triangle { 44 | position: absolute; 45 | border-width: 7px; 46 | border-style: solid; 47 | border-color: transparent transparent rgb(255, 255, 255); 48 | border-image: initial; 49 | } 50 | .hide-triangle > .triangle { 51 | display: none; 52 | } 53 | .hide-triangle > .triangleShadow { 54 | display: none; 55 | } 56 | .top-left-triangle > .triangle { 57 | top: -14px; 58 | left: 10px; 59 | } 60 | .top-left-triangle > .triangleShadow { 61 | top: -16px; 62 | left: 9px; 63 | } 64 | .top-right-triangle > .triangle { 65 | top: -14px; 66 | right: 10px; 67 | } 68 | .top-right-triangle > .triangleShadow { 69 | top: -16px; 70 | right: 9px; 71 | } 72 | .bottom-right-triangle > .triangle { 73 | top: 35px; 74 | right: 10px; 75 | transform: rotate(180deg); 76 | } 77 | .bottom-right-triangle > .triangleShadow { 78 | top: 37px; 79 | right: 9px; 80 | transform: rotate(180deg); 81 | } 82 | `, 83 | ], 84 | changeDetection: ChangeDetectionStrategy.OnPush, 85 | preserveWhitespaces: false, 86 | providers: [ 87 | { 88 | provide: NG_VALUE_ACCESSOR, 89 | useExisting: forwardRef(() => GithubComponent), 90 | multi: true, 91 | }, 92 | { 93 | provide: ColorWrap, 94 | useExisting: forwardRef(() => GithubComponent), 95 | }, 96 | ], 97 | standalone: false, 98 | }) 99 | export class GithubComponent extends ColorWrap { 100 | /** Pixel value for picker width */ 101 | @Input() width: string | number = 212; 102 | /** Color squares to display */ 103 | @Input() colors = [ 104 | '#B80000', 105 | '#DB3E00', 106 | '#FCCB00', 107 | '#008B02', 108 | '#006B76', 109 | '#1273DE', 110 | '#004DCF', 111 | '#5300EB', 112 | '#EB9694', 113 | '#FAD0C3', 114 | '#FEF3BD', 115 | '#C1E1C5', 116 | '#BEDADC', 117 | '#C4DEF6', 118 | '#BED3F3', 119 | '#D4C4FB', 120 | ]; 121 | @Input() triangle: 'hide' | 'top-left' | 'top-right' | 'bottom-right' = 'top-left'; 122 | 123 | constructor() { 124 | super(); 125 | } 126 | 127 | handleBlockChange({ hex, $event }: { hex: string; $event: Event }) { 128 | if (isValidHex(hex)) { 129 | this.handleChange({ hex, source: 'hex' }, $event); 130 | } 131 | } 132 | handleValueChange({ data, $event }) { 133 | this.handleChange(data, $event); 134 | } 135 | } 136 | 137 | @NgModule({ 138 | declarations: [GithubComponent, GithubSwatchComponent], 139 | exports: [GithubComponent, GithubSwatchComponent], 140 | imports: [CommonModule, SwatchModule], 141 | }) 142 | export class ColorGithubModule {} 143 | -------------------------------------------------------------------------------- /src/lib/github/github.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorGithubModule } from './github.component'; 6 | 7 | describe('BlockComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [GithubTestApp], 11 | imports: [ColorGithubModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(GithubTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.github-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class GithubTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/github/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/github/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/github", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/github/public_api.ts: -------------------------------------------------------------------------------- 1 | export { GithubSwatchComponent } from './github-swatch.component'; 2 | export { ColorGithubModule, GithubComponent } from './github.component'; 3 | -------------------------------------------------------------------------------- /src/lib/helpers/checkboard.ts: -------------------------------------------------------------------------------- 1 | const checkboardCache: { [key: string]: string } = {}; 2 | 3 | export function render(c1: string, c2: string, size: number) { 4 | if (typeof document === 'undefined') { 5 | return null; 6 | } 7 | const canvas = document.createElement('canvas'); 8 | canvas.width = size * 2; 9 | canvas.height = size * 2; 10 | const ctx = canvas.getContext('2d'); 11 | if (!ctx) { 12 | return null; 13 | } // If no context can be found, return early. 14 | ctx.fillStyle = c1; 15 | ctx.fillRect(0, 0, canvas.width, canvas.height); 16 | ctx.fillStyle = c2; 17 | ctx.fillRect(0, 0, size, size); 18 | ctx.translate(size, size); 19 | ctx.fillRect(0, 0, size, size); 20 | return canvas.toDataURL(); 21 | } 22 | 23 | export function getCheckerboard(c1: string, c2: string, size: number) { 24 | const key = `${c1}-${c2}-${size}`; 25 | if (checkboardCache[key]) { 26 | return checkboardCache[key]; 27 | } 28 | const checkboard = render(c1, c2, size); 29 | if (!checkboard) { 30 | return null; 31 | } 32 | checkboardCache[key] = checkboard; 33 | return checkboard; 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/helpers/color.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface RGB { 2 | r: number; 3 | g: number; 4 | b: number; 5 | } 6 | 7 | export interface RGBA extends RGB { 8 | a: number; 9 | } 10 | 11 | export interface HSL { 12 | h: number; 13 | s: number; 14 | l: number; 15 | } 16 | 17 | export interface HSLA extends HSL { 18 | a: number; 19 | } 20 | 21 | export interface HSV { 22 | h: number; 23 | s: number; 24 | v: number; 25 | } 26 | 27 | export interface HSVA extends HSV { 28 | a: number; 29 | } 30 | 31 | export interface HSVAsource extends HSVA { 32 | source: string; 33 | } 34 | 35 | export interface HSLAsource extends HSLA { 36 | source: string; 37 | } 38 | 39 | export interface Color { 40 | hex: string; 41 | rgb: RGBA; 42 | hsl: HSLA; 43 | hsv: HSVA; 44 | oldHue: number; 45 | source: string; 46 | } 47 | 48 | export interface Shape { 49 | color: string; 50 | title: string; 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/helpers/color.ts: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor'; 2 | 3 | import { Color } from './color.interfaces'; 4 | 5 | export function simpleCheckForValidColor(data) { 6 | const keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'l', 'v']; 7 | let checked = 0; 8 | let passed = 0; 9 | keysToCheck.forEach(letter => { 10 | if (!data[letter]) { 11 | return; 12 | } 13 | checked += 1; 14 | if (!isNaN(data[letter])) { 15 | passed += 1; 16 | } 17 | if (letter === 's' || letter === 'l') { 18 | const percentPatt = /^\d+%$/; 19 | if (percentPatt.test(data[letter])) { 20 | passed += 1; 21 | } 22 | } 23 | }); 24 | return checked === passed ? data : false; 25 | } 26 | 27 | export function toState(data, oldHue?: number, disableAlpha?: boolean): Color { 28 | const color = data.hex ? new TinyColor(data.hex) : new TinyColor(data); 29 | if (disableAlpha) { 30 | color.setAlpha(1); 31 | } 32 | 33 | const hsl = color.toHsl(); 34 | const hsv = color.toHsv(); 35 | const rgb = color.toRgb(); 36 | const hex = color.toHex(); 37 | if (hsl.s === 0) { 38 | hsl.h = oldHue || 0; 39 | hsv.h = oldHue || 0; 40 | } 41 | const transparent = hex === '000000' && rgb.a === 0; 42 | 43 | return { 44 | hsl, 45 | hex: transparent ? 'transparent' : color.toHexString(), 46 | rgb, 47 | hsv, 48 | oldHue: data.h || oldHue || hsl.h, 49 | source: data.source, 50 | }; 51 | } 52 | 53 | export function isValidHex(hex: string) { 54 | return new TinyColor(hex).isValid; 55 | } 56 | 57 | export function getContrastingColor(data) { 58 | if (!data) { 59 | return '#fff'; 60 | } 61 | const col = toState(data); 62 | if (col.hex === 'transparent') { 63 | return 'rgba(0,0,0,0.4)'; 64 | } 65 | const yiq = (col.rgb.r * 299 + col.rgb.g * 587 + col.rgb.b * 114) / 1000; 66 | return yiq >= 128 ? '#000' : '#fff'; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/hue.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | Output, 10 | } from '@angular/core'; 11 | 12 | import { CoordinatesModule } from './coordinates.directive'; 13 | import { HSLA, HSLAsource } from './helpers/color.interfaces'; 14 | 15 | @Component({ 16 | selector: 'color-hue', 17 | template: ` 18 |
23 |
28 | @if (!hidePointer) { 29 |
30 |
31 |
32 | } 33 |
34 |
35 | `, 36 | styles: [ 37 | ` 38 | .color-hue { 39 | position: absolute; 40 | top: 0; 41 | bottom: 0; 42 | left: 0; 43 | right: 0; 44 | } 45 | .color-hue-container { 46 | margin: 0 2px; 47 | position: relative; 48 | height: 100%; 49 | } 50 | .color-hue-pointer { 51 | position: absolute; 52 | } 53 | .color-hue-slider { 54 | margin-top: 1px; 55 | width: 4px; 56 | border-radius: 1px; 57 | height: 8px; 58 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); 59 | background: #fff; 60 | transform: translateX(-2px); 61 | } 62 | .color-hue-horizontal { 63 | background: linear-gradient( 64 | to right, 65 | #f00 0%, 66 | #ff0 17%, 67 | #0f0 33%, 68 | #0ff 50%, 69 | #00f 67%, 70 | #f0f 83%, 71 | #f00 100% 72 | ); 73 | } 74 | .color-hue-vertical { 75 | background: linear-gradient( 76 | to top, 77 | #f00 0%, 78 | #ff0 17%, 79 | #0f0 33%, 80 | #0ff 50%, 81 | #00f 67%, 82 | #f0f 83%, 83 | #f00 100% 84 | ); 85 | } 86 | `, 87 | ], 88 | preserveWhitespaces: false, 89 | changeDetection: ChangeDetectionStrategy.OnPush, 90 | standalone: false, 91 | }) 92 | export class HueComponent implements OnChanges { 93 | @Input() hsl!: HSLA; 94 | @Input() pointer!: Record; 95 | @Input() radius!: number; 96 | @Input() shadow!: string; 97 | @Input() hidePointer = false; 98 | @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; 99 | @Output() onChange = new EventEmitter<{ data: HSLAsource; $event: Event }>(); 100 | left = '0px'; 101 | top = ''; 102 | 103 | ngOnChanges(): void { 104 | if (this.direction === 'horizontal') { 105 | this.left = `${(this.hsl.h * 100) / 360}%`; 106 | } else { 107 | this.top = `${-((this.hsl.h * 100) / 360) + 100}%`; 108 | } 109 | } 110 | handleChange({ top, left, containerHeight, containerWidth, $event }): void { 111 | let data: HSLAsource | undefined; 112 | if (this.direction === 'vertical') { 113 | let h: number; 114 | if (top < 0) { 115 | h = 359; 116 | } else if (top > containerHeight) { 117 | h = 0; 118 | } else { 119 | const percent = -((top * 100) / containerHeight) + 100; 120 | h = (360 * percent) / 100; 121 | } 122 | 123 | if (this.hsl.h !== h) { 124 | data = { 125 | h, 126 | s: this.hsl.s, 127 | l: this.hsl.l, 128 | a: this.hsl.a, 129 | source: 'rgb', 130 | }; 131 | } 132 | } else { 133 | let h: number; 134 | if (left < 0) { 135 | h = 0; 136 | } else if (left > containerWidth) { 137 | h = 359; 138 | } else { 139 | const percent = (left * 100) / containerWidth; 140 | h = (360 * percent) / 100; 141 | } 142 | 143 | if (this.hsl.h !== h) { 144 | data = { 145 | h, 146 | s: this.hsl.s, 147 | l: this.hsl.l, 148 | a: this.hsl.a, 149 | source: 'rgb', 150 | }; 151 | } 152 | } 153 | 154 | if (!data) { 155 | return; 156 | } 157 | 158 | this.onChange.emit({ data, $event }); 159 | } 160 | } 161 | 162 | @NgModule({ 163 | declarations: [HueComponent], 164 | exports: [HueComponent], 165 | imports: [CommonModule, CoordinatesModule], 166 | }) 167 | export class HueModule {} 168 | -------------------------------------------------------------------------------- /src/lib/hue/hue-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | forwardRef, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | } from '@angular/core'; 10 | 11 | import { ColorWrap, HueModule, toState } from 'ngx-color'; 12 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 13 | 14 | @Component({ 15 | selector: 'color-hue-picker', 16 | template: ` 17 |
18 | 25 |
26 | `, 27 | styles: [ 28 | ` 29 | .hue-picker { 30 | position: relative; 31 | } 32 | `, 33 | ], 34 | changeDetection: ChangeDetectionStrategy.OnPush, 35 | preserveWhitespaces: false, 36 | providers: [ 37 | { 38 | provide: NG_VALUE_ACCESSOR, 39 | useExisting: forwardRef(() => HuePickerComponent), 40 | multi: true, 41 | }, 42 | { 43 | provide: ColorWrap, 44 | useExisting: forwardRef(() => HuePickerComponent), 45 | }, 46 | ], 47 | standalone: false, 48 | }) 49 | export class HuePickerComponent extends ColorWrap implements OnChanges { 50 | /** Pixel value for picker width */ 51 | @Input() width: string | number = 316; 52 | /** Pixel value for picker height */ 53 | @Input() height: string | number = 16; 54 | @Input() radius = 2; 55 | @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; 56 | pointer: { [key: string]: string } = { 57 | width: '18px', 58 | height: '18px', 59 | borderRadius: '50%', 60 | transform: 'translate(-9px, -2px)', 61 | backgroundColor: 'rgb(248, 248, 248)', 62 | boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.37)', 63 | }; 64 | 65 | constructor() { 66 | super(); 67 | } 68 | 69 | ngOnChanges() { 70 | if (this.direction === 'vertical') { 71 | this.pointer.transform = 'translate(-3px, -9px)'; 72 | } 73 | this.setState(toState(this.color, this.oldHue)); 74 | } 75 | handlePickerChange({ data, $event }) { 76 | this.handleChange({ a: 1, h: data.h, l: 0.5, s: 1 }, $event); 77 | } 78 | } 79 | 80 | @NgModule({ 81 | declarations: [HuePickerComponent], 82 | exports: [HuePickerComponent], 83 | imports: [CommonModule, HueModule], 84 | }) 85 | export class ColorHueModule {} 86 | -------------------------------------------------------------------------------- /src/lib/hue/hue.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorHueModule } from './hue-picker.component'; 6 | 7 | describe('HuePickerComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [HueTestApp], 11 | imports: [ColorHueModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(HueTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.hue-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class HueTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/hue/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/hue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/hue", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/hue/public_api.ts: -------------------------------------------------------------------------------- 1 | export { ColorHueModule, HuePickerComponent } from './hue-picker.component'; 2 | -------------------------------------------------------------------------------- /src/lib/material/material.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { ColorWrap, EditableInputModule, isValidHex, RaisedModule, zDepth } from 'ngx-color'; 5 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'color-material', 9 | template: ` 10 | 11 |
12 | 18 |
19 |
20 | 26 |
27 |
28 | 34 |
35 |
36 | 42 |
43 |
44 |
45 |
46 | `, 47 | styles: [ 48 | ` 49 | .material-picker { 50 | width: 130px; 51 | height: 130px; 52 | padding: 16px; 53 | font-family: Roboto; 54 | } 55 | .material-split { 56 | display: flex; 57 | margin-right: -10px; 58 | padding-top: 11px; 59 | } 60 | .material-third { 61 | flex: 1 1 0%; 62 | padding-right: 10px; 63 | } 64 | `, 65 | ], 66 | changeDetection: ChangeDetectionStrategy.OnPush, 67 | preserveWhitespaces: false, 68 | providers: [ 69 | { 70 | provide: NG_VALUE_ACCESSOR, 71 | useExisting: forwardRef(() => MaterialComponent), 72 | multi: true, 73 | }, 74 | { 75 | provide: ColorWrap, 76 | useExisting: forwardRef(() => MaterialComponent), 77 | }, 78 | ], 79 | standalone: false, 80 | }) 81 | export class MaterialComponent extends ColorWrap { 82 | HEXinput: { [key: string]: string } = { 83 | width: '100%', 84 | marginTop: '12px', 85 | fontSize: '15px', 86 | color: 'rgb(51, 51, 51)', 87 | padding: '0px', 88 | 'border-width': '0px 0px 2px', 89 | outline: 'none', 90 | height: '30px', 91 | }; 92 | HEXlabel: { [key: string]: string } = { 93 | position: 'absolute', 94 | top: '0px', 95 | left: '0px', 96 | fontSize: '11px', 97 | color: 'rgb(153, 153, 153)', 98 | 'text-transform': 'capitalize', 99 | }; 100 | RGBinput: { [key: string]: string } = { 101 | width: '100%', 102 | marginTop: '12px', 103 | fontSize: '15px', 104 | color: '#333', 105 | padding: '0px', 106 | border: '0px', 107 | 'border-bottom': '1px solid #eee', 108 | outline: 'none', 109 | height: '30px', 110 | }; 111 | RGBlabel: { [key: string]: string } = { 112 | position: 'absolute', 113 | top: '0px', 114 | left: '0px', 115 | fontSize: '11px', 116 | color: '#999999', 117 | 'text-transform': 'capitalize', 118 | }; 119 | @Input() zDepth: zDepth = 1; 120 | @Input() radius = 1; 121 | @Input() background = '#fff'; 122 | disableAlpha = true; 123 | 124 | constructor() { 125 | super(); 126 | } 127 | 128 | handleValueChange({ data, $event }) { 129 | this.handleChange(data, $event); 130 | } 131 | 132 | handleInputChange({ data, $event }) { 133 | if (data.hex) { 134 | if (isValidHex(data.hex)) { 135 | this.handleValueChange({ 136 | data: { 137 | hex: data.hex, 138 | source: 'hex', 139 | }, 140 | $event, 141 | }); 142 | } 143 | } else if (data.r || data.g || data.b) { 144 | this.handleValueChange({ 145 | data: { 146 | r: data.r || this.rgb.r, 147 | g: data.g || this.rgb.g, 148 | b: data.b || this.rgb.b, 149 | source: 'rgb', 150 | }, 151 | $event, 152 | }); 153 | } 154 | } 155 | 156 | afterValidChange() { 157 | this.HEXinput['border-bottom-color'] = this.hex; 158 | } 159 | } 160 | 161 | @NgModule({ 162 | exports: [MaterialComponent], 163 | declarations: [MaterialComponent], 164 | imports: [CommonModule, EditableInputModule, RaisedModule], 165 | }) 166 | export class ColorMaterialModule {} 167 | -------------------------------------------------------------------------------- /src/lib/material/material.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorMaterialModule } from './material.component'; 6 | 7 | describe('MaterialComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [MaterialTestApp], 11 | imports: [ColorMaterialModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(MaterialTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.material-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class MaterialTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/material/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/material/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/material", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/material/public_api.ts: -------------------------------------------------------------------------------- 1 | export { ColorMaterialModule, MaterialComponent } from './material.component'; 2 | -------------------------------------------------------------------------------- /src/lib/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | }, 6 | "allowedNonPeerDependencies": ["@ctrl/tinycolor", "material-colors"], 7 | "dest": "../../dist" 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color", 3 | "version": "0.0.0-placeholder", 4 | "description": "A Collection of Color Pickers from Sketch, Photoshop, Chrome & more", 5 | "dependencies": { 6 | "@ctrl/tinycolor": "^4.1.0", 7 | "material-colors": "^1.2.6" 8 | }, 9 | "peerDependencies": { 10 | "@angular/core": ">=19.0.0-0", 11 | "@angular/common": ">=19.0.0-0" 12 | }, 13 | "homepage": "https://github.com/scttcper/ngx-color", 14 | "repository": "scttcper/ngx-color", 15 | "license": "MIT", 16 | "keywords": [ 17 | "angular", 18 | "color picker", 19 | "angular-component", 20 | "colorpicker", 21 | "picker", 22 | "sketch", 23 | "chrome", 24 | "photoshop", 25 | "material design", 26 | "popup" 27 | ], 28 | "publishConfig": { 29 | "access": "public", 30 | "provenance": true 31 | }, 32 | "release": { 33 | "branches": [ 34 | "master" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/photoshop/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/photoshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/photoshop", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/photoshop/photoshop-button.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'color-photoshop-button', 5 | template: ` 6 |
7 | {{ label }} 8 |
9 | `, 10 | styles: [ 11 | ` 12 | .photoshop-button { 13 | background-image: linear-gradient(-180deg, rgb(255, 255, 255) 0%, rgb(230, 230, 230) 100%); 14 | border: 1px solid rgb(135, 135, 135); 15 | border-radius: 2px; 16 | height: 22px; 17 | box-shadow: rgb(234, 234, 234) 0px 1px 0px 0px; 18 | font-size: 14px; 19 | color: rgb(0, 0, 0); 20 | line-height: 20px; 21 | text-align: center; 22 | margin-bottom: 10px; 23 | cursor: pointer; 24 | } 25 | .photoshop-button.active { 26 | box-shadow: 0 0 0 1px #878787; 27 | } 28 | `, 29 | ], 30 | changeDetection: ChangeDetectionStrategy.OnPush, 31 | preserveWhitespaces: false, 32 | standalone: false, 33 | }) 34 | export class PhotoshopButtonComponent { 35 | @Input() label = ''; 36 | @Input() active = false; 37 | @Output() onClick = new EventEmitter(); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/photoshop/photoshop-fields.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { isValidHex, HSV, RGB } from 'ngx-color'; 4 | 5 | @Component({ 6 | selector: 'color-photoshop-fields', 7 | template: ` 8 |
9 | 15 | 21 | 27 |
28 | 34 | 40 | 46 |
47 | 53 |
54 |
°
55 |
%
56 |
%
57 |
58 |
59 | `, 60 | styles: [ 61 | ` 62 | .photoshop-fields { 63 | padding-top: 5px; 64 | padding-bottom: 9px; 65 | width: 85px; 66 | position: relative; 67 | } 68 | .photoshop-field-symbols { 69 | position: absolute; 70 | top: 5px; 71 | right: -7px; 72 | font-size: 13px; 73 | } 74 | .photoshop-symbol { 75 | height: 24px; 76 | line-height: 24px; 77 | padding-bottom: 7px; 78 | } 79 | .photoshop-divider { 80 | height: 5px; 81 | } 82 | `, 83 | ], 84 | changeDetection: ChangeDetectionStrategy.OnPush, 85 | preserveWhitespaces: false, 86 | standalone: false, 87 | }) 88 | export class PhotoshopFieldsComponent { 89 | @Input() rgb!: RGB; 90 | @Input() hsv!: HSV; 91 | @Input() hex!: string; 92 | @Output() onChange = new EventEmitter(); 93 | RGBinput: Record = { 94 | marginLeft: '35%', 95 | width: '40%', 96 | height: '22px', 97 | border: '1px solid rgb(136, 136, 136)', 98 | boxShadow: 'rgba(0, 0, 0, 0.1) 0px 1px 1px inset, rgb(236, 236, 236) 0px 1px 0px 0px', 99 | marginBottom: '2px', 100 | fontSize: '13px', 101 | paddingLeft: '3px', 102 | marginRight: '10px', 103 | }; 104 | RGBwrap: Record = { 105 | position: 'relative', 106 | }; 107 | RGBlabel: Record = { 108 | left: '0px', 109 | width: '34px', 110 | textTransform: 'uppercase', 111 | fontSize: '13px', 112 | height: '24px', 113 | lineHeight: '24px', 114 | position: 'absolute', 115 | }; 116 | HEXinput: Record = { 117 | marginLeft: '20%', 118 | width: '80%', 119 | height: '22px', 120 | border: '1px solid #888888', 121 | boxShadow: 'inset 0 1px 1px rgba(0,0,0,.1), 0 1px 0 0 #ECECEC', 122 | marginBottom: '3px', 123 | fontSize: '13px', 124 | paddingLeft: '3px', 125 | }; 126 | HEXwrap: Record = { 127 | position: 'relative', 128 | }; 129 | HEXlabel: Record = { 130 | position: 'absolute', 131 | top: '0px', 132 | left: '0px', 133 | width: '14px', 134 | textTransform: 'uppercase', 135 | fontSize: '13px', 136 | height: '24px', 137 | lineHeight: '24px', 138 | }; 139 | 140 | round(v) { 141 | return Math.round(v); 142 | } 143 | handleValueChange({ data, $event }) { 144 | if (data['#']) { 145 | if (isValidHex(data['#'])) { 146 | this.onChange.emit({ 147 | data: { 148 | hex: data['#'], 149 | source: 'hex', 150 | }, 151 | $event, 152 | }); 153 | } 154 | } else if (data.r || data.g || data.b) { 155 | this.onChange.emit({ 156 | data: { 157 | r: data.r || this.rgb.r, 158 | g: data.g || this.rgb.g, 159 | b: data.b || this.rgb.b, 160 | source: 'rgb', 161 | }, 162 | $event, 163 | }); 164 | } else if (data.h || data.s || data.v) { 165 | this.onChange.emit({ 166 | data: { 167 | h: data.h || this.hsv.h, 168 | s: data.s || this.hsv.s, 169 | v: data.v || this.hsv.v, 170 | source: 'hsv', 171 | }, 172 | $event, 173 | }); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/lib/photoshop/photoshop-previews.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; 2 | import { RGB } from 'ngx-color'; 3 | 4 | @Component({ 5 | selector: 'color-photoshop-previews', 6 | template: ` 7 |
8 |
new
9 |
10 |
11 |
12 |
13 |
current
14 |
15 | `, 16 | styles: [ 17 | ` 18 | .photoshop-swatches { 19 | border: 1px solid #b3b3b3; 20 | border-bottom: 1px solid #f0f0f0; 21 | margin-bottom: 2px; 22 | margin-top: 1px; 23 | } 24 | .photoshop-new { 25 | height: 34px; 26 | box-shadow: 27 | inset 1px 0 0 #000, 28 | inset -1px 0 0 #000, 29 | inset 0 1px 0 #000; 30 | } 31 | .photoshop-current { 32 | height: 34px; 33 | box-shadow: 34 | inset 1px 0 0 #000, 35 | inset -1px 0 0 #000, 36 | inset 0 -1px 0 #000; 37 | } 38 | .photoshop-label { 39 | font-size: 14px; 40 | color: #000; 41 | text-align: center; 42 | } 43 | `, 44 | ], 45 | changeDetection: ChangeDetectionStrategy.OnPush, 46 | preserveWhitespaces: false, 47 | standalone: false, 48 | }) 49 | export class PhotoshopPreviewsComponent implements OnChanges { 50 | @Input() rgb!: RGB; 51 | @Input() currentColor = ''; 52 | backgroundNew = ''; 53 | 54 | ngOnChanges() { 55 | this.backgroundNew = `rgb(${this.rgb.r},${this.rgb.g}, ${this.rgb.b})`; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/photoshop/photoshop.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | forwardRef, 7 | Input, 8 | NgModule, 9 | Output, 10 | } from '@angular/core'; 11 | 12 | import { 13 | AlphaModule, 14 | ColorWrap, 15 | EditableInputModule, 16 | HueModule, 17 | SaturationModule, 18 | SwatchModule, 19 | } from 'ngx-color'; 20 | import { PhotoshopButtonComponent } from './photoshop-button.component'; 21 | import { PhotoshopFieldsComponent } from './photoshop-fields.component'; 22 | import { PhotoshopPreviewsComponent } from './photoshop-previews.component'; 23 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 24 | 25 | @Component({ 26 | selector: 'color-photoshop', 27 | template: ` 28 |
29 |
{{ header }}
30 |
31 |
32 | 38 |
39 |
40 | 46 |
47 |
48 |
49 |
50 | 54 |
55 |
56 | 61 | 62 | 63 | 69 |
70 |
71 |
72 |
73 |
74 | `, 75 | styles: [ 76 | ` 77 | .photoshop-picker { 78 | background: rgb(220, 220, 220); 79 | border-radius: 4px; 80 | box-shadow: 81 | rgba(0, 0, 0, 0.25) 0px 0px 0px 1px, 82 | rgba(0, 0, 0, 0.15) 0px 8px 16px; 83 | box-sizing: initial; 84 | width: 513px; 85 | } 86 | .photoshop-head { 87 | background-image: linear-gradient(-180deg, rgb(240, 240, 240) 0%, rgb(212, 212, 212) 100%); 88 | border-bottom: 1px solid rgb(177, 177, 177); 89 | box-shadow: 90 | rgba(255, 255, 255, 0.2) 0px 1px 0px 0px inset, 91 | rgba(0, 0, 0, 0.02) 0px -1px 0px 0px inset; 92 | height: 23px; 93 | line-height: 24px; 94 | border-radius: 4px 4px 0px 0px; 95 | font-size: 13px; 96 | color: rgb(77, 77, 77); 97 | text-align: center; 98 | } 99 | .photoshop-body { 100 | padding: 15px 15px 0px; 101 | display: flex; 102 | } 103 | .photoshop-saturation { 104 | width: 256px; 105 | height: 256px; 106 | position: relative; 107 | border-width: 2px; 108 | border-style: solid; 109 | border-color: rgb(179, 179, 179) rgb(179, 179, 179) rgb(240, 240, 240); 110 | border-image: initial; 111 | overflow: hidden; 112 | } 113 | .photoshop-hue { 114 | position: relative; 115 | height: 256px; 116 | width: 23px; 117 | margin-left: 10px; 118 | border-width: 2px; 119 | border-style: solid; 120 | border-color: rgb(179, 179, 179) rgb(179, 179, 179) rgb(240, 240, 240); 121 | border-image: initial; 122 | } 123 | .photoshop-controls { 124 | width: 180px; 125 | margin-left: 10px; 126 | } 127 | .photoshop-top { 128 | display: flex; 129 | } 130 | .photoshop-previews { 131 | width: 60px; 132 | } 133 | .photoshop-actions { 134 | -webkit-box-flex: 1; 135 | flex: 1 1 0%; 136 | margin-left: 20px; 137 | } 138 | `, 139 | ], 140 | changeDetection: ChangeDetectionStrategy.OnPush, 141 | preserveWhitespaces: false, 142 | providers: [ 143 | { 144 | provide: NG_VALUE_ACCESSOR, 145 | useExisting: forwardRef(() => PhotoshopComponent), 146 | multi: true, 147 | }, 148 | { 149 | provide: ColorWrap, 150 | useExisting: forwardRef(() => PhotoshopComponent), 151 | }, 152 | ], 153 | standalone: false, 154 | }) 155 | export class PhotoshopComponent extends ColorWrap { 156 | /** Title text */ 157 | @Input() header = 'Color Picker'; 158 | @Output() onAccept = new EventEmitter(); 159 | @Output() onCancel = new EventEmitter(); 160 | circle = { 161 | width: '12px', 162 | height: '12px', 163 | borderRadius: '6px', 164 | boxShadow: 'rgb(255, 255, 255) 0px 0px 0px 1px inset', 165 | transform: 'translate(-6px, -10px)', 166 | }; 167 | constructor() { 168 | super(); 169 | } 170 | handleValueChange({ data, $event }) { 171 | this.handleChange(data, $event); 172 | } 173 | } 174 | 175 | @NgModule({ 176 | declarations: [ 177 | PhotoshopComponent, 178 | PhotoshopPreviewsComponent, 179 | PhotoshopButtonComponent, 180 | PhotoshopFieldsComponent, 181 | ], 182 | exports: [ 183 | PhotoshopComponent, 184 | PhotoshopPreviewsComponent, 185 | PhotoshopButtonComponent, 186 | PhotoshopFieldsComponent, 187 | ], 188 | imports: [ 189 | CommonModule, 190 | EditableInputModule, 191 | HueModule, 192 | AlphaModule, 193 | SwatchModule, 194 | SaturationModule, 195 | ], 196 | }) 197 | export class ColorPhotoshopModule {} 198 | -------------------------------------------------------------------------------- /src/lib/photoshop/photoshop.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorPhotoshopModule } from './photoshop.component'; 6 | 7 | describe('PhotoshopComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [PhotoshopTestApp], 11 | imports: [ColorPhotoshopModule], 12 | }).compileComponents(); 13 | })); 14 | it('should apply className to root element', () => { 15 | const fixture = TestBed.createComponent(PhotoshopTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.photoshop-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class PhotoshopTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/photoshop/public_api.ts: -------------------------------------------------------------------------------- 1 | export { PhotoshopButtonComponent } from './photoshop-button.component'; 2 | export { PhotoshopFieldsComponent } from './photoshop-fields.component'; 3 | export { PhotoshopPreviewsComponent } from './photoshop-previews.component'; 4 | export { ColorPhotoshopModule, PhotoshopComponent } from './photoshop.component'; 5 | -------------------------------------------------------------------------------- /src/lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './alpha.component'; 2 | export * from './checkboard.component'; 3 | export * from './color-wrap.component'; 4 | export * from './editable-input.component'; 5 | export * from './hue.component'; 6 | export * from './raised.component'; 7 | export * from './saturation.component'; 8 | export * from './swatch.component'; 9 | export * from './coordinates.directive'; 10 | export * from './shade.component'; 11 | 12 | export * from './helpers/checkboard'; 13 | export * from './helpers/color'; 14 | export * from './helpers/color.interfaces'; 15 | -------------------------------------------------------------------------------- /src/lib/raised.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, Input, NgModule } from '@angular/core'; 3 | 4 | export type zDepth = 0 | 1 | 2 | 3 | 4 | 5; 5 | 6 | @Component({ 7 | selector: 'color-raised', 8 | template: ` 9 |
10 |
11 |
12 | 13 |
14 |
15 | `, 16 | styles: [ 17 | ` 18 | .raised-wrap { 19 | position: relative; 20 | display: inline-block; 21 | } 22 | .raised-bg { 23 | position: absolute; 24 | top: 0px; 25 | right: 0px; 26 | bottom: 0px; 27 | left: 0px; 28 | } 29 | .raised-content { 30 | position: relative; 31 | } 32 | .zDepth-0 { 33 | box-shadow: none; 34 | } 35 | .zDepth-1 { 36 | box-shadow: 37 | 0 2px 10px rgba(0, 0, 0, 0.12), 38 | 0 2px 5px rgba(0, 0, 0, 0.16); 39 | } 40 | .zDepth-2 { 41 | box-shadow: 42 | 0 6px 20px rgba(0, 0, 0, 0.19), 43 | 0 8px 17px rgba(0, 0, 0, 0.2); 44 | } 45 | .zDepth-3 { 46 | box-shadow: 47 | 0 17px 50px rgba(0, 0, 0, 0.19), 48 | 0 12px 15px rgba(0, 0, 0, 0.24); 49 | } 50 | .zDepth-4 { 51 | box-shadow: 52 | 0 25px 55px rgba(0, 0, 0, 0.21), 53 | 0 16px 28px rgba(0, 0, 0, 0.22); 54 | } 55 | .zDepth-5 { 56 | box-shadow: 57 | 0 40px 77px rgba(0, 0, 0, 0.22), 58 | 0 27px 24px rgba(0, 0, 0, 0.2); 59 | } 60 | `, 61 | ], 62 | preserveWhitespaces: false, 63 | changeDetection: ChangeDetectionStrategy.OnPush, 64 | standalone: false, 65 | }) 66 | export class RaisedComponent { 67 | @Input() zDepth: zDepth = 1; 68 | @Input() radius = 1; 69 | @Input() background = '#fff'; 70 | } 71 | 72 | @NgModule({ 73 | declarations: [RaisedComponent], 74 | exports: [RaisedComponent], 75 | imports: [CommonModule], 76 | }) 77 | export class RaisedModule {} 78 | -------------------------------------------------------------------------------- /src/lib/saturation.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | Output, 10 | } from '@angular/core'; 11 | 12 | import { CoordinatesModule } from './coordinates.directive'; 13 | import { HSLA, HSVA, HSVAsource } from './helpers/color.interfaces'; 14 | 15 | @Component({ 16 | selector: 'color-saturation', 17 | template: ` 18 |
24 |
25 |
26 |
32 |
33 |
34 |
35 |
36 | `, 37 | styles: [ 38 | ` 39 | .saturation-white { 40 | background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); 41 | position: absolute; 42 | top: 0; 43 | bottom: 0; 44 | left: 0; 45 | right: 0; 46 | } 47 | .saturation-black { 48 | background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); 49 | position: absolute; 50 | top: 0; 51 | bottom: 0; 52 | left: 0; 53 | right: 0; 54 | } 55 | .color-saturation { 56 | position: absolute; 57 | top: 0; 58 | bottom: 0; 59 | left: 0; 60 | right: 0; 61 | } 62 | .saturation-pointer { 63 | position: absolute; 64 | cursor: default; 65 | } 66 | .saturation-circle { 67 | width: 4px; 68 | height: 4px; 69 | box-shadow: 70 | 0 0 0 1.5px #fff, 71 | inset 0 0 1px 1px rgba(0, 0, 0, 0.3), 72 | 0 0 1px 2px rgba(0, 0, 0, 0.4); 73 | border-radius: 50%; 74 | cursor: hand; 75 | transform: translate(-2px, -4px); 76 | } 77 | `, 78 | ], 79 | preserveWhitespaces: false, 80 | changeDetection: ChangeDetectionStrategy.OnPush, 81 | standalone: false, 82 | }) 83 | export class SaturationComponent implements OnChanges { 84 | @Input() hsl!: HSLA; 85 | @Input() hsv!: HSVA; 86 | @Input() radius!: number; 87 | @Input() pointer!: Record; 88 | @Input() circle!: Record; 89 | @Output() onChange = new EventEmitter<{ data: HSVAsource; $event: Event }>(); 90 | background!: string; 91 | pointerTop!: string; 92 | pointerLeft!: string; 93 | 94 | ngOnChanges() { 95 | this.background = `hsl(${this.hsl.h}, 100%, 50%)`; 96 | this.pointerTop = -(this.hsv.v * 100) + 1 + 100 + '%'; 97 | this.pointerLeft = this.hsv.s * 100 + '%'; 98 | } 99 | handleChange({ top, left, containerHeight, containerWidth, $event }) { 100 | if (left < 0) { 101 | left = 0; 102 | } else if (left > containerWidth) { 103 | left = containerWidth; 104 | } else if (top < 0) { 105 | top = 0; 106 | } else if (top > containerHeight) { 107 | top = containerHeight; 108 | } 109 | 110 | const saturation = left / containerWidth; 111 | let bright = -(top / containerHeight) + 1; 112 | bright = bright > 0 ? bright : 0; 113 | bright = bright > 1 ? 1 : bright; 114 | 115 | const data: HSVAsource = { 116 | h: this.hsl.h, 117 | s: saturation, 118 | v: bright, 119 | a: this.hsl.a, 120 | source: 'hsva', 121 | }; 122 | this.onChange.emit({ data, $event }); 123 | } 124 | } 125 | 126 | @NgModule({ 127 | declarations: [SaturationComponent], 128 | exports: [SaturationComponent], 129 | imports: [CommonModule, CoordinatesModule], 130 | }) 131 | export class SaturationModule {} 132 | -------------------------------------------------------------------------------- /src/lib/shade.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | Output, 10 | } from '@angular/core'; 11 | import { CoordinatesModule } from './coordinates.directive'; 12 | import { HSLA, RGBA } from './helpers/color.interfaces'; 13 | import { TinyColor } from '@ctrl/tinycolor'; 14 | 15 | @Component({ 16 | selector: 'color-shade', 17 | template: ` 18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | `, 32 | styles: [ 33 | ` 34 | .shade { 35 | position: absolute; 36 | top: 0; 37 | bottom: 0; 38 | left: 0; 39 | right: 0; 40 | } 41 | .shade-gradient { 42 | position: absolute; 43 | top: 0; 44 | bottom: 0; 45 | left: 0; 46 | right: 0; 47 | } 48 | .shade-container { 49 | position: relative; 50 | height: 100%; 51 | margin: 0 3px; 52 | } 53 | .shade-pointer { 54 | position: absolute; 55 | } 56 | .shade-slider { 57 | width: 4px; 58 | border-radius: 1px; 59 | height: 8px; 60 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); 61 | background: #fff; 62 | margin-top: 1px; 63 | transform: translateX(-2px); 64 | } 65 | `, 66 | ], 67 | changeDetection: ChangeDetectionStrategy.OnPush, 68 | preserveWhitespaces: false, 69 | standalone: false, 70 | }) 71 | export class ShadeComponent implements OnChanges { 72 | @Input() hsl!: HSLA; 73 | @Input() rgb!: RGBA; 74 | @Input() pointer!: Record; 75 | @Input() shadow!: string; 76 | @Input() radius!: string; 77 | @Output() onChange = new EventEmitter(); 78 | gradient!: Record; 79 | pointerLeft!: number; 80 | pointerTop?: number; 81 | 82 | ngOnChanges() { 83 | this.gradient = { 84 | background: `linear-gradient(to right, 85 | hsl(${this.hsl.h}, 90%, 55%), 86 | #000)`, 87 | }; 88 | const hsv = new TinyColor(this.hsl).toHsv(); 89 | this.pointerLeft = 100 - hsv.v * 100; 90 | } 91 | 92 | handleChange({ left, containerWidth, $event }): void { 93 | let data; 94 | let v: number; 95 | if (left < 0) { 96 | v = 0; 97 | } else if (left > containerWidth) { 98 | v = 1; 99 | } else { 100 | v = Math.round((left * 100) / containerWidth) / 100; 101 | } 102 | 103 | const hsv = new TinyColor(this.hsl).toHsv(); 104 | if (hsv.v !== v) { 105 | data = { 106 | h: this.hsl.h, 107 | s: 100, 108 | v: 1 - v, 109 | l: this.hsl.l, 110 | a: this.hsl.a, 111 | source: 'rgb', 112 | }; 113 | } 114 | 115 | if (!data) { 116 | return; 117 | } 118 | 119 | this.onChange.emit({ data, $event }); 120 | } 121 | } 122 | 123 | @NgModule({ 124 | declarations: [ShadeComponent], 125 | exports: [ShadeComponent], 126 | imports: [CommonModule, CoordinatesModule], 127 | }) 128 | export class ShadeModule {} 129 | -------------------------------------------------------------------------------- /src/lib/shade/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/shade/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/shade", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "license": "MIT" 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/shade/public_api.ts: -------------------------------------------------------------------------------- 1 | export { ColorShadeModule, ShadeSliderComponent } from './shade-picker.component'; 2 | -------------------------------------------------------------------------------- /src/lib/shade/shade-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | forwardRef, 6 | Input, 7 | NgModule, 8 | OnChanges, 9 | } from '@angular/core'; 10 | import { ColorWrap, ShadeModule, toState } from 'ngx-color'; 11 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 12 | 13 | @Component({ 14 | selector: 'color-shade-picker', 15 | template: ` 16 |
21 | 27 |
28 | `, 29 | styles: [ 30 | ` 31 | .shade-slider { 32 | position: relative; 33 | } 34 | `, 35 | ], 36 | changeDetection: ChangeDetectionStrategy.OnPush, 37 | preserveWhitespaces: false, 38 | providers: [ 39 | { 40 | provide: NG_VALUE_ACCESSOR, 41 | useExisting: forwardRef(() => ShadeSliderComponent), 42 | multi: true, 43 | }, 44 | { 45 | provide: ColorWrap, 46 | useExisting: forwardRef(() => ShadeSliderComponent), 47 | }, 48 | ], 49 | standalone: false, 50 | }) 51 | export class ShadeSliderComponent extends ColorWrap implements OnChanges { 52 | /** Pixel value for picker width */ 53 | @Input() width: string | number = 316; 54 | /** Pixel value for picker height */ 55 | @Input() height: string | number = 16; 56 | pointer: { [key: string]: string } = { 57 | width: '18px', 58 | height: '18px', 59 | borderRadius: '50%', 60 | transform: 'translate(-9px, -2px)', 61 | boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.37)', 62 | }; 63 | 64 | constructor() { 65 | super(); 66 | } 67 | ngOnChanges() { 68 | this.setState(toState(this.color, this.oldHue)); 69 | } 70 | handlePickerChange({ data, $event }) { 71 | this.handleChange(data, $event); 72 | } 73 | } 74 | 75 | @NgModule({ 76 | declarations: [ShadeSliderComponent], 77 | exports: [ShadeSliderComponent], 78 | imports: [CommonModule, ShadeModule], 79 | }) 80 | export class ColorShadeModule {} 81 | -------------------------------------------------------------------------------- /src/lib/shade/shade-picker.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { ColorShadeModule } from './shade-picker.component'; 5 | 6 | export const red = { 7 | hsl: { a: 1, h: 0, l: 0.5, s: 1 }, 8 | hex: '#ff0000', 9 | rgb: { r: 255, g: 0, b: 0, a: 1 }, 10 | hsv: { h: 0, s: 1, v: 1, a: 1 }, 11 | }; 12 | 13 | describe('AlphaComponent', () => { 14 | beforeEach(waitForAsync(() => { 15 | TestBed.configureTestingModule({ 16 | declarations: [ColorShadeSliderApp], 17 | imports: [ColorShadeModule], 18 | }).compileComponents(); 19 | })); 20 | it(`should apply className to root element`, () => { 21 | const fixture = TestBed.createComponent(ColorShadeSliderApp); 22 | fixture.detectChanges(); 23 | const compiled = fixture.nativeElement; 24 | expect(compiled.querySelector('.shade-slider').className).toContain('classy'); 25 | }); 26 | }); 27 | 28 | @Component({ 29 | selector: 'test-app', 30 | template: ``, 31 | standalone: false, 32 | }) 33 | class ColorShadeSliderApp { 34 | className = 'classy'; 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/sketch/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/sketch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/sketch", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/sketch/public_api.ts: -------------------------------------------------------------------------------- 1 | export { SketchFieldsComponent } from './sketch-fields.component'; 2 | export { SketchPresetColorsComponent } from './sketch-preset-colors.component'; 3 | export { ColorSketchModule, SketchComponent } from './sketch.component'; 4 | -------------------------------------------------------------------------------- /src/lib/sketch/sketch-fields.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { isValidHex, HSLA, RGBA } from 'ngx-color'; 4 | import { TinyColor } from '@ctrl/tinycolor'; 5 | 6 | @Component({ 7 | selector: 'color-sketch-fields', 8 | template: ` 9 |
10 |
11 | 17 |
18 |
19 | 27 |
28 |
29 | 37 |
38 |
39 | 47 |
48 | @if (disableAlpha === false) { 49 |
50 | 58 |
59 | } 60 |
61 | `, 62 | styles: [ 63 | ` 64 | .sketch-fields { 65 | display: flex; 66 | padding-top: 4px; 67 | } 68 | .sketch-double { 69 | -webkit-box-flex: 2; 70 | flex: 2 1 0%; 71 | } 72 | .sketch-single { 73 | flex: 1 1 0%; 74 | padding-left: 6px; 75 | } 76 | .sketch-alpha { 77 | -webkit-box-flex: 1; 78 | flex: 1 1 0%; 79 | padding-left: 6px; 80 | } 81 | :host-context([dir='rtl']) .sketch-single { 82 | padding-right: 6px; 83 | padding-left: 0; 84 | } 85 | :host-context([dir='rtl']) .sketch-alpha { 86 | padding-right: 6px; 87 | padding-left: 0; 88 | } 89 | `, 90 | ], 91 | changeDetection: ChangeDetectionStrategy.OnPush, 92 | preserveWhitespaces: false, 93 | standalone: false, 94 | }) 95 | export class SketchFieldsComponent { 96 | @Input() hsl!: HSLA; 97 | @Input() rgb!: RGBA; 98 | @Input() hex!: string; 99 | @Input() disableAlpha = false; 100 | @Output() onChange = new EventEmitter(); 101 | input: { [key: string]: string } = { 102 | width: '100%', 103 | padding: '4px 10% 3px', 104 | border: 'none', 105 | boxSizing: 'border-box', 106 | boxShadow: 'inset 0 0 0 1px #ccc', 107 | fontSize: '11px', 108 | }; 109 | label: { [key: string]: string } = { 110 | display: 'block', 111 | textAlign: 'center', 112 | fontSize: '11px', 113 | color: '#222', 114 | paddingTop: '3px', 115 | paddingBottom: '4px', 116 | textTransform: 'capitalize', 117 | }; 118 | 119 | round(value) { 120 | return Math.round(value); 121 | } 122 | handleChange({ data, $event }) { 123 | if (data.hex) { 124 | if (isValidHex(data.hex)) { 125 | const color = new TinyColor(data.hex); 126 | this.onChange.emit({ 127 | data: { 128 | hex: this.disableAlpha || data.hex.length <= 6 ? color.toHex() : color.toHex8(), 129 | source: 'hex', 130 | }, 131 | $event, 132 | }); 133 | } 134 | } else if (data.r || data.g || data.b) { 135 | this.onChange.emit({ 136 | data: { 137 | r: data.r || this.rgb.r, 138 | g: data.g || this.rgb.g, 139 | b: data.b || this.rgb.b, 140 | source: 'rgb', 141 | }, 142 | $event, 143 | }); 144 | } else if (data.a) { 145 | if (data.a < 0) { 146 | data.a = 0; 147 | } else if (data.a > 100) { 148 | data.a = 100; 149 | } 150 | data.a /= 100; 151 | 152 | if (this.disableAlpha) { 153 | data.a = 1; 154 | } 155 | 156 | this.onChange.emit({ 157 | data: { 158 | h: this.hsl.h, 159 | s: this.hsl.s, 160 | l: this.hsl.l, 161 | a: Math.round(data.a * 100) / 100, 162 | source: 'rgb', 163 | }, 164 | $event, 165 | }); 166 | } else if (data.h || data.s || data.l) { 167 | this.onChange.emit({ 168 | data: { 169 | h: data.h || this.hsl.h, 170 | s: Number((data.s && data.s) || this.hsl.s), 171 | l: Number((data.l && data.l) || this.hsl.l), 172 | source: 'hsl', 173 | }, 174 | $event, 175 | }); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/lib/sketch/sketch-preset-colors.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { Shape } from 'ngx-color'; 4 | 5 | @Component({ 6 | selector: 'color-sketch-preset-colors', 7 | template: ` 8 |
9 | @for (c of colors; track c) { 10 |
11 | 19 |
20 | } 21 |
22 | `, 23 | styles: [ 24 | ` 25 | .sketch-swatches { 26 | position: relative; 27 | display: flex; 28 | flex-wrap: wrap; 29 | margin: 0px -10px; 30 | padding: 10px 0px 0px 10px; 31 | border-top: 1px solid rgb(238, 238, 238); 32 | } 33 | .sketch-wrap { 34 | width: 16px; 35 | height: 16px; 36 | margin: 0px 10px 10px 0px; 37 | } 38 | :host-context([dir='rtl']) .sketch-swatches { 39 | padding-right: 10px; 40 | padding-left: 0; 41 | } 42 | :host-context([dir='rtl']) .sketch-wrap { 43 | margin-left: 10px; 44 | margin-right: 0; 45 | } 46 | `, 47 | ], 48 | changeDetection: ChangeDetectionStrategy.OnPush, 49 | preserveWhitespaces: false, 50 | standalone: false, 51 | }) 52 | export class SketchPresetColorsComponent { 53 | @Input() colors!: string[]; 54 | @Output() onClick = new EventEmitter(); 55 | @Output() onSwatchHover = new EventEmitter(); 56 | swatchStyle = { 57 | borderRadius: '3px', 58 | boxShadow: 'inset 0 0 0 1px rgba(0,0,0,.15)', 59 | }; 60 | 61 | handleClick({ hex, $event }) { 62 | this.onClick.emit({ hex, $event }); 63 | } 64 | normalizeValue(val: string | Shape) { 65 | if (typeof val === 'string') { 66 | return { color: val }; 67 | } 68 | return val; 69 | } 70 | focusStyle(val: string | Shape) { 71 | const c = this.normalizeValue(val); 72 | return { 73 | boxShadow: `inset 0 0 0 1px rgba(0,0,0,.15), 0 0 4px ${c.color}`, 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/lib/sketch/sketch.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { 5 | AlphaModule, 6 | CheckboardModule, 7 | ColorWrap, 8 | EditableInputModule, 9 | HueModule, 10 | isValidHex, 11 | SaturationModule, 12 | SwatchModule, 13 | } from 'ngx-color'; 14 | import { SketchFieldsComponent } from './sketch-fields.component'; 15 | import { SketchPresetColorsComponent } from './sketch-preset-colors.component'; 16 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 17 | 18 | @Component({ 19 | selector: 'color-sketch', 20 | template: ` 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 |
31 | @if (disableAlpha === false) { 32 |
33 | 39 |
40 | } 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 55 |
56 | @if (presetColors && presetColors.length) { 57 |
58 | 63 |
64 | } 65 |
66 | `, 67 | styles: [ 68 | ` 69 | .sketch-picker { 70 | padding: 10px 10px 3px; 71 | box-sizing: initial; 72 | background: #fff; 73 | border-radius: 4px; 74 | box-shadow: 75 | 0 0 0 1px rgba(0, 0, 0, 0.15), 76 | 0 8px 16px rgba(0, 0, 0, 0.15); 77 | } 78 | .sketch-saturation { 79 | width: 100%; 80 | padding-bottom: 75%; 81 | position: relative; 82 | overflow: hidden; 83 | } 84 | .sketch-fields-container { 85 | display: block; 86 | } 87 | .sketch-swatches-container { 88 | display: block; 89 | } 90 | .sketch-controls { 91 | display: flex; 92 | } 93 | .sketch-sliders { 94 | padding: 4px 0px; 95 | -webkit-box-flex: 1; 96 | flex: 1 1 0%; 97 | } 98 | .sketch-hue { 99 | position: relative; 100 | height: 10px; 101 | overflow: hidden; 102 | } 103 | .sketch-alpha { 104 | position: relative; 105 | height: 10px; 106 | margin-top: 4px; 107 | overflow: hidden; 108 | } 109 | .sketch-color { 110 | width: 24px; 111 | height: 24px; 112 | position: relative; 113 | margin-top: 4px; 114 | margin-left: 4px; 115 | border-radius: 3px; 116 | } 117 | .sketch-active { 118 | position: absolute; 119 | top: 0px; 120 | right: 0px; 121 | bottom: 0px; 122 | left: 0px; 123 | border-radius: 2px; 124 | box-shadow: 125 | rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset, 126 | rgba(0, 0, 0, 0.25) 0px 0px 4px inset; 127 | } 128 | :host-context([dir='rtl']) .sketch-color { 129 | margin-right: 4px; 130 | margin-left: 0; 131 | } 132 | `, 133 | ], 134 | changeDetection: ChangeDetectionStrategy.OnPush, 135 | preserveWhitespaces: false, 136 | providers: [ 137 | { 138 | provide: NG_VALUE_ACCESSOR, 139 | useExisting: forwardRef(() => SketchComponent), 140 | multi: true, 141 | }, 142 | { 143 | provide: ColorWrap, 144 | useExisting: forwardRef(() => SketchComponent), 145 | }, 146 | ], 147 | standalone: false, 148 | }) 149 | export class SketchComponent extends ColorWrap { 150 | /** Remove alpha slider and options from picker */ 151 | @Input() disableAlpha = false; 152 | /** Hex strings for default colors at bottom of picker */ 153 | @Input() presetColors = [ 154 | '#D0021B', 155 | '#F5A623', 156 | '#F8E71C', 157 | '#8B572A', 158 | '#7ED321', 159 | '#417505', 160 | '#BD10E0', 161 | '#9013FE', 162 | '#4A90E2', 163 | '#50E3C2', 164 | '#B8E986', 165 | '#000000', 166 | '#4A4A4A', 167 | '#9B9B9B', 168 | '#FFFFFF', 169 | ]; 170 | /** Width of picker */ 171 | @Input() width = 200; 172 | activeBackground!: string; 173 | constructor() { 174 | super(); 175 | } 176 | afterValidChange() { 177 | const alpha = this.disableAlpha ? 1 : this.rgb.a; 178 | this.activeBackground = `rgba(${this.rgb.r}, ${this.rgb.g}, ${this.rgb.b}, ${alpha})`; 179 | } 180 | handleValueChange({ data, $event }) { 181 | this.handleChange(data, $event); 182 | } 183 | handleBlockChange({ hex, $event }) { 184 | if (isValidHex(hex)) { 185 | // this.hex = hex; 186 | this.handleChange( 187 | { 188 | hex, 189 | source: 'hex', 190 | }, 191 | $event, 192 | ); 193 | } 194 | } 195 | } 196 | 197 | @NgModule({ 198 | declarations: [SketchComponent, SketchFieldsComponent, SketchPresetColorsComponent], 199 | exports: [SketchComponent, SketchFieldsComponent, SketchPresetColorsComponent], 200 | imports: [ 201 | CommonModule, 202 | AlphaModule, 203 | CheckboardModule, 204 | EditableInputModule, 205 | HueModule, 206 | SaturationModule, 207 | SwatchModule, 208 | ], 209 | }) 210 | export class ColorSketchModule {} 211 | -------------------------------------------------------------------------------- /src/lib/sketch/sketch.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorSketchModule } from './sketch.component'; 6 | 7 | describe('SketchComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [SketchTestApp], 11 | imports: [ColorSketchModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(SketchTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.sketch-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class SketchTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/slider/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/slider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/slider", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/slider/public_api.ts: -------------------------------------------------------------------------------- 1 | export { SliderSwatchComponent } from './slider-swatch.component'; 2 | export { SliderSwatchesComponent } from './slider-swatches.component'; 3 | export { ColorSliderModule, SliderComponent } from './slider.component'; 4 | -------------------------------------------------------------------------------- /src/lib/slider/slider-swatch.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnChanges, 7 | Output, 8 | } from '@angular/core'; 9 | 10 | import { HSL } from 'ngx-color'; 11 | 12 | @Component({ 13 | selector: 'color-slider-swatch', 14 | template: ` 15 |
23 | `, 24 | styles: [ 25 | ` 26 | .slider-swatch { 27 | height: 12px; 28 | background: rgb(121, 211, 166); 29 | cursor: pointer; 30 | } 31 | .slider-swatch.active { 32 | transform: scaleY(1.8); 33 | border-top-right-radius: 3.6px 2px; 34 | border-top-left-radius: 3.6px 2px; 35 | border-bottom-right-radius: 3.6px 2px; 36 | border-bottom-left-radius: 3.6px 2px; 37 | } 38 | .slider-swatch.first { 39 | border-radius: 2px 0px 0px 2px; 40 | } 41 | .slider-swatch.last { 42 | border-radius: 0px 2px 2px 0px; 43 | } 44 | `, 45 | ], 46 | changeDetection: ChangeDetectionStrategy.OnPush, 47 | preserveWhitespaces: false, 48 | standalone: false, 49 | }) 50 | export class SliderSwatchComponent implements OnChanges { 51 | @Input() hsl!: HSL; 52 | @Input() active!: boolean; 53 | @Input() offset!: number; 54 | @Input() first = false; 55 | @Input() last = false; 56 | @Output() onClick = new EventEmitter(); 57 | background!: string; 58 | 59 | ngOnChanges() { 60 | this.background = `hsl(${this.hsl.h}, 50%, ${this.offset * 100}%)`; 61 | } 62 | handleClick($event) { 63 | this.onClick.emit({ 64 | data: { 65 | h: this.hsl.h, 66 | s: 0.5, 67 | l: this.offset, 68 | source: 'hsl', 69 | }, 70 | $event, 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/slider/slider-swatches.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | import { HSL } from 'ngx-color'; 4 | 5 | @Component({ 6 | selector: 'color-slider-swatches', 7 | template: ` 8 |
9 |
10 | 17 |
18 |
19 | 25 |
26 |
27 | 33 |
34 |
35 | 41 |
42 |
43 | 50 |
51 |
52 | `, 53 | styles: [ 54 | ` 55 | .slider-swatches { 56 | margin-top: 20px; 57 | } 58 | .slider-swatch-wrap { 59 | box-sizing: border-box; 60 | width: 20%; 61 | padding-right: 1px; 62 | float: left; 63 | } 64 | `, 65 | ], 66 | changeDetection: ChangeDetectionStrategy.OnPush, 67 | preserveWhitespaces: false, 68 | standalone: false, 69 | }) 70 | export class SliderSwatchesComponent { 71 | @Input() hsl!: HSL; 72 | @Output() onClick = new EventEmitter(); 73 | @Output() onSwatchHover = new EventEmitter(); 74 | 75 | active(l: number, s: number) { 76 | return Math.round(this.hsl.l * 100) / 100 === l && Math.round(this.hsl.s * 100) / 100 === s; 77 | } 78 | handleClick({ data, $event }) { 79 | this.onClick.emit({ data, $event }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/slider/slider.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { ColorWrap, HueModule, SwatchModule } from 'ngx-color'; 5 | import { SliderSwatchComponent } from './slider-swatch.component'; 6 | import { SliderSwatchesComponent } from './slider-swatches.component'; 7 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 8 | 9 | @Component({ 10 | selector: 'color-slider', 11 | template: ` 12 |
13 |
14 | 20 |
21 |
22 | 26 |
27 |
28 | `, 29 | styles: [ 30 | ` 31 | .slider-hue { 32 | height: 12px; 33 | position: relative; 34 | } 35 | `, 36 | ], 37 | changeDetection: ChangeDetectionStrategy.OnPush, 38 | preserveWhitespaces: false, 39 | providers: [ 40 | { 41 | provide: NG_VALUE_ACCESSOR, 42 | useExisting: forwardRef(() => SliderComponent), 43 | multi: true, 44 | }, 45 | { 46 | provide: ColorWrap, 47 | useExisting: forwardRef(() => SliderComponent), 48 | }, 49 | ], 50 | standalone: false, 51 | }) 52 | export class SliderComponent extends ColorWrap { 53 | @Input() 54 | pointer: Record = { 55 | width: '14px', 56 | height: '14px', 57 | borderRadius: '6px', 58 | transform: 'translate(-7px, -2px)', 59 | backgroundColor: 'rgb(248, 248, 248)', 60 | boxShadow: '0 1px 4px 0 rgba(0, 0, 0, 0.37)', 61 | }; 62 | @Input() radius = 2; 63 | 64 | constructor() { 65 | super(); 66 | } 67 | 68 | handlePickerChange({ data, $event }) { 69 | this.handleChange(data, $event); 70 | } 71 | } 72 | 73 | @NgModule({ 74 | declarations: [SliderComponent, SliderSwatchComponent, SliderSwatchesComponent], 75 | exports: [SliderComponent, SliderSwatchComponent, SliderSwatchesComponent], 76 | imports: [CommonModule, HueModule, SwatchModule], 77 | }) 78 | export class ColorSliderModule {} 79 | -------------------------------------------------------------------------------- /src/lib/slider/slider.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorSliderModule } from './slider.component'; 6 | 7 | describe('SliderComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [SliderTestApp], 11 | imports: [ColorSliderModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(SliderTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.slider-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ` `, 25 | standalone: false, 26 | }) 27 | class SliderTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/swatch.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ChangeDetectionStrategy, 4 | Component, 5 | EventEmitter, 6 | Input, 7 | NgModule, 8 | OnInit, 9 | Output, 10 | } from '@angular/core'; 11 | 12 | import { CheckboardModule } from './checkboard.component'; 13 | 14 | @Component({ 15 | selector: 'color-swatch', 16 | template: ` 17 |
28 | 29 | @if (color === 'transparent') { 30 | 31 | } 32 |
33 | `, 34 | styles: [ 35 | ` 36 | .swatch { 37 | outline: none; 38 | height: 100%; 39 | width: 100%; 40 | cursor: pointer; 41 | position: relative; 42 | } 43 | `, 44 | ], 45 | changeDetection: ChangeDetectionStrategy.OnPush, 46 | standalone: false, 47 | }) 48 | export class SwatchComponent implements OnInit { 49 | @Input() color!: string; 50 | @Input() style: Record = {}; 51 | @Input() focusStyle: Record = {}; 52 | @Input() focus!: boolean; 53 | @Output() onClick = new EventEmitter(); 54 | @Output() onHover = new EventEmitter(); 55 | divStyles: Record = {}; 56 | focusStyles: Record = {}; 57 | inFocus = false; 58 | 59 | ngOnInit() { 60 | this.divStyles = { 61 | background: this.color as string, 62 | ...this.style, 63 | }; 64 | } 65 | currentStyles() { 66 | this.focusStyles = { 67 | ...this.divStyles, 68 | ...this.focusStyle, 69 | }; 70 | return this.focus || this.inFocus ? this.focusStyles : this.divStyles; 71 | } 72 | handleFocusOut() { 73 | this.inFocus = false; 74 | } 75 | handleFocus() { 76 | this.inFocus = true; 77 | } 78 | handleHover(hex: string, $event) { 79 | this.onHover.emit({ hex, $event }); 80 | } 81 | handleClick(hex: string, $event) { 82 | this.onClick.emit({ hex, $event }); 83 | } 84 | } 85 | 86 | @NgModule({ 87 | declarations: [SwatchComponent], 88 | exports: [SwatchComponent], 89 | imports: [CommonModule, CheckboardModule], 90 | }) 91 | export class SwatchModule {} 92 | -------------------------------------------------------------------------------- /src/lib/swatches/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/swatches/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/swatches", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | }, 8 | "dependencies": { 9 | "material-colors": "^1.2.6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/swatches/public_api.ts: -------------------------------------------------------------------------------- 1 | export { SwatchesColorComponent } from './swatches-color.component'; 2 | export { SwatchesGroupComponent } from './swatches-group.component'; 3 | export { ColorSwatchesModule, SwatchesComponent } from './swatches.component'; 4 | -------------------------------------------------------------------------------- /src/lib/swatches/swatches-color.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | } from '@angular/core'; 9 | 10 | import { getContrastingColor } from 'ngx-color'; 11 | 12 | @Component({ 13 | selector: 'color-swatches-color', 14 | template: ` 15 | 25 | @if (active) { 26 |
27 | 32 | 33 | 34 |
35 | } 36 |
37 | `, 38 | styles: [ 39 | ` 40 | .swatches-group { 41 | padding-bottom: 10px; 42 | width: 40px; 43 | float: left; 44 | margin-right: 10px; 45 | } 46 | .swatch-check { 47 | display: flex; 48 | margin-left: 8px; 49 | } 50 | `, 51 | ], 52 | changeDetection: ChangeDetectionStrategy.OnPush, 53 | preserveWhitespaces: false, 54 | standalone: false, 55 | }) 56 | export class SwatchesColorComponent implements OnInit { 57 | @Input() color!: string; 58 | @Input() first = false; 59 | @Input() last = false; 60 | @Input() active!: boolean; 61 | @Output() onClick = new EventEmitter(); 62 | @Output() onSwatchHover = new EventEmitter(); 63 | getContrastingColor = getContrastingColor; 64 | colorStyle: Record = { 65 | width: '40px', 66 | height: '24px', 67 | cursor: 'pointer', 68 | marginBottom: '1px', 69 | }; 70 | focusStyle: Record = {}; 71 | 72 | ngOnInit() { 73 | this.colorStyle.background = this.color; 74 | this.focusStyle.boxShadow = `0 0 4px ${this.color}`; 75 | if (this.first) { 76 | this.colorStyle.borderRadius = '2px 2px 0 0'; 77 | } 78 | if (this.last) { 79 | this.colorStyle.borderRadius = '0 0 2px 2px'; 80 | } 81 | if (this.color === '#FFFFFF') { 82 | this.colorStyle.boxShadow = 'inset 0 0 0 1px #ddd'; 83 | } 84 | } 85 | handleClick($event) { 86 | this.onClick.emit({ 87 | data: { 88 | hex: this.color, 89 | source: 'hex', 90 | }, 91 | $event, 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/lib/swatches/swatches-group.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'color-swatches-group', 5 | template: ` 6 |
7 | @for (color of group; track color; let idx = $index) { 8 | 15 | 16 | } 17 |
18 | `, 19 | styles: [ 20 | ` 21 | .swatches-group { 22 | padding-bottom: 10px; 23 | width: 40px; 24 | float: left; 25 | margin-right: 10px; 26 | } 27 | `, 28 | ], 29 | changeDetection: ChangeDetectionStrategy.OnPush, 30 | preserveWhitespaces: false, 31 | standalone: false, 32 | }) 33 | export class SwatchesGroupComponent { 34 | @Input() group!: string[]; 35 | @Input() active!: string; 36 | @Output() onClick = new EventEmitter(); 37 | @Output() onSwatchHover = new EventEmitter(); 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/swatches/swatches.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | import { 4 | amber, 5 | blue, 6 | blueGrey, 7 | brown, 8 | cyan, 9 | deepOrange, 10 | deepPurple, 11 | green, 12 | indigo, 13 | lightBlue, 14 | lightGreen, 15 | lime, 16 | orange, 17 | pink, 18 | purple, 19 | red, 20 | teal, 21 | yellow, 22 | } from 'material-colors'; 23 | 24 | import { ColorWrap, RaisedModule, SwatchModule, zDepth } from 'ngx-color'; 25 | import { SwatchesColorComponent } from './swatches-color.component'; 26 | import { SwatchesGroupComponent } from './swatches-group.component'; 27 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 28 | 29 | @Component({ 30 | selector: 'color-swatches', 31 | template: ` 32 |
37 | 38 |
39 |
40 | @for (group of colors; track group) { 41 | 46 | } 47 |
48 |
49 |
50 |
51 | `, 52 | styles: [ 53 | ` 54 | .swatches-overflow { 55 | overflow-y: scroll; 56 | } 57 | .swatches-overflow { 58 | padding: 16px 0 6px 16px; 59 | } 60 | `, 61 | ], 62 | changeDetection: ChangeDetectionStrategy.OnPush, 63 | preserveWhitespaces: false, 64 | providers: [ 65 | { 66 | provide: NG_VALUE_ACCESSOR, 67 | useExisting: forwardRef(() => SwatchesComponent), 68 | multi: true, 69 | }, 70 | { 71 | provide: ColorWrap, 72 | useExisting: forwardRef(() => SwatchesComponent), 73 | }, 74 | ], 75 | standalone: false, 76 | }) 77 | export class SwatchesComponent extends ColorWrap { 78 | /** Pixel value for picker width */ 79 | @Input() width: string | number = 320; 80 | /** Color squares to display */ 81 | @Input() height: string | number = 240; 82 | /** An array of color groups, each with an array of colors */ 83 | @Input() 84 | colors: string[][] = [ 85 | [red['900'], red['700'], red['500'], red['300'], red['100']], 86 | [pink['900'], pink['700'], pink['500'], pink['300'], pink['100']], 87 | [purple['900'], purple['700'], purple['500'], purple['300'], purple['100']], 88 | [deepPurple['900'], deepPurple['700'], deepPurple['500'], deepPurple['300'], deepPurple['100']], 89 | [indigo['900'], indigo['700'], indigo['500'], indigo['300'], indigo['100']], 90 | [blue['900'], blue['700'], blue['500'], blue['300'], blue['100']], 91 | [lightBlue['900'], lightBlue['700'], lightBlue['500'], lightBlue['300'], lightBlue['100']], 92 | [cyan['900'], cyan['700'], cyan['500'], cyan['300'], cyan['100']], 93 | [teal['900'], teal['700'], teal['500'], teal['300'], teal['100']], 94 | ['#194D33', green['700'], green['500'], green['300'], green['100']], 95 | [lightGreen['900'], lightGreen['700'], lightGreen['500'], lightGreen['300'], lightGreen['100']], 96 | [lime['900'], lime['700'], lime['500'], lime['300'], lime['100']], 97 | [yellow['900'], yellow['700'], yellow['500'], yellow['300'], yellow['100']], 98 | [amber['900'], amber['700'], amber['500'], amber['300'], amber['100']], 99 | [orange['900'], orange['700'], orange['500'], orange['300'], orange['100']], 100 | [deepOrange['900'], deepOrange['700'], deepOrange['500'], deepOrange['300'], deepOrange['100']], 101 | [brown['900'], brown['700'], brown['500'], brown['300'], brown['100']], 102 | [blueGrey['900'], blueGrey['700'], blueGrey['500'], blueGrey['300'], blueGrey['100']], 103 | ['#000000', '#525252', '#969696', '#D9D9D9', '#FFFFFF'], 104 | ]; 105 | @Input() zDepth: zDepth = 1; 106 | @Input() radius = 1; 107 | @Input() background = '#fff'; 108 | 109 | constructor() { 110 | super(); 111 | } 112 | 113 | handlePickerChange({ data, $event }) { 114 | this.handleChange(data, $event); 115 | } 116 | } 117 | 118 | @NgModule({ 119 | declarations: [SwatchesComponent, SwatchesGroupComponent, SwatchesColorComponent], 120 | exports: [SwatchesComponent, SwatchesGroupComponent, SwatchesColorComponent], 121 | imports: [CommonModule, SwatchModule, RaisedModule], 122 | }) 123 | export class ColorSwatchesModule {} 124 | -------------------------------------------------------------------------------- /src/lib/swatches/swatches.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { ColorSwatchesModule } from './swatches.component'; 5 | 6 | describe('SwatchesComponent', () => { 7 | beforeEach(waitForAsync(() => { 8 | TestBed.configureTestingModule({ 9 | declarations: [SwatchTestApp], 10 | imports: [ColorSwatchesModule], 11 | }).compileComponents(); 12 | })); 13 | it(`should apply className to root element`, () => { 14 | const fixture = TestBed.createComponent(SwatchTestApp); 15 | fixture.detectChanges(); 16 | const compiled = fixture.nativeElement; 17 | expect(compiled.querySelector('.swatches-picker').className).toContain('classy'); 18 | }); 19 | }); 20 | 21 | @Component({ 22 | selector: 'test-app', 23 | template: ``, 24 | standalone: false, 25 | }) 26 | class SwatchTestApp { 27 | className = 'classy'; 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/twitter/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/twitter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-color/twitter", 3 | "author": "scttcper", 4 | "repository": { 5 | "type": "git", 6 | "url": "git+https://github.com/scttcper/ngx-color.git" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/twitter/public_api.ts: -------------------------------------------------------------------------------- 1 | export { ColorTwitterModule, TwitterComponent } from './twitter.component'; 2 | -------------------------------------------------------------------------------- /src/lib/twitter/twitter.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, forwardRef, Input, NgModule } from '@angular/core'; 3 | 4 | import { ColorWrap, EditableInputModule, isValidHex, SwatchModule } from 'ngx-color'; 5 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 6 | 7 | @Component({ 8 | selector: 'color-twitter', 9 | template: ` 10 | 37 | `, 38 | styles: [ 39 | ` 40 | .twitter-picker { 41 | background: rgb(255, 255, 255); 42 | border: 0px solid rgba(0, 0, 0, 0.25); 43 | box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 4px; 44 | border-radius: 4px; 45 | position: relative; 46 | box-sizing: border-box; 47 | } 48 | .triangleShadow { 49 | width: 0px; 50 | height: 0px; 51 | border-style: solid; 52 | border-width: 0px 9px 10px; 53 | border-color: transparent transparent rgba(0, 0, 0, 0.1); 54 | position: absolute; 55 | } 56 | .triangle { 57 | width: 0px; 58 | height: 0px; 59 | border-style: solid; 60 | border-width: 0px 9px 10px; 61 | border-color: transparent transparent rgb(255, 255, 255); 62 | position: absolute; 63 | } 64 | .hide-triangle > .triangle { 65 | display: none; 66 | } 67 | .hide-triangle > .triangleShadow { 68 | display: none; 69 | } 70 | .top-left-triangle > .triangle { 71 | top: -10px; 72 | left: 12px; 73 | } 74 | .top-left-triangle > .triangleShadow { 75 | top: -11px; 76 | left: 12px; 77 | } 78 | .top-right-triangle > .triangle { 79 | top: -10px; 80 | right: 12px; 81 | } 82 | .top-right-triangle > .triangleShadow { 83 | top: -11px; 84 | right: 12px; 85 | } 86 | .twitter-body { 87 | padding: 15px 9px 9px 15px; 88 | } 89 | .twitter-swatch { 90 | width: 30px; 91 | height: 30px; 92 | display: inline-block; 93 | margin: 0 6px 0 0; 94 | } 95 | .twitter-hash { 96 | background: rgb(240, 240, 240); 97 | height: 30px; 98 | width: 30px; 99 | border-radius: 4px 0px 0px 4px; 100 | color: rgb(152, 161, 164); 101 | margin-left: -3px; 102 | display: inline-block; 103 | } 104 | .twitter-hash > div { 105 | position: absolute; 106 | align-items: center; 107 | justify-content: center; 108 | height: 30px; 109 | width: 30px; 110 | display: flex; 111 | } 112 | .twitter-input { 113 | position: relative; 114 | display: inline-block; 115 | margin-top: -6px; 116 | font-size: 10px; 117 | height: 27px; 118 | padding: 0; 119 | position: relative; 120 | top: 6px; 121 | vertical-align: top; 122 | width: 108px; 123 | margin-left: -4px; 124 | } 125 | `, 126 | ], 127 | changeDetection: ChangeDetectionStrategy.OnPush, 128 | preserveWhitespaces: false, 129 | providers: [ 130 | { 131 | provide: NG_VALUE_ACCESSOR, 132 | useExisting: forwardRef(() => TwitterComponent), 133 | multi: true, 134 | }, 135 | { 136 | provide: ColorWrap, 137 | useExisting: forwardRef(() => TwitterComponent), 138 | }, 139 | ], 140 | standalone: false, 141 | }) 142 | export class TwitterComponent extends ColorWrap { 143 | /** Pixel value for picker width */ 144 | @Input() width: string | number = 276; 145 | /** Color squares to display */ 146 | @Input() colors = [ 147 | '#FF6900', 148 | '#FCB900', 149 | '#7BDCB5', 150 | '#00D084', 151 | '#8ED1FC', 152 | '#0693E3', 153 | '#ABB8C3', 154 | '#EB144C', 155 | '#F78DA7', 156 | '#9900EF', 157 | ]; 158 | @Input() triangle: 'hide' | 'top-left' | 'top-right' | 'bottom-right' = 'top-left'; 159 | 160 | swatchStyle: { [key: string]: string } = { 161 | width: '30px', 162 | height: '30px', 163 | borderRadius: '4px', 164 | fontSize: '0', 165 | }; 166 | input: { [key: string]: string } = { 167 | borderRadius: '4px', 168 | borderBottomLeftRadius: '0', 169 | borderTopLeftRadius: '0', 170 | border: '1px solid #e6ecf0', 171 | boxSizing: 'border-box', 172 | display: 'inline', 173 | fontSize: '14px', 174 | height: '30px', 175 | padding: '0', 176 | paddingLeft: '6px', 177 | width: '100%', 178 | color: '#657786', 179 | }; 180 | disableAlpha = true; 181 | 182 | constructor() { 183 | super(); 184 | } 185 | 186 | focus(color: string) { 187 | return { boxShadow: `0 0 4px ${color}` }; 188 | } 189 | 190 | handleBlockChange({ hex, $event }: any) { 191 | if (isValidHex(hex)) { 192 | // this.hex = hex; 193 | this.handleChange({ hex, source: 'hex' }, $event); 194 | } 195 | } 196 | 197 | handleValueChange({ data, $event }: any) { 198 | this.handleBlockChange({ hex: data, $event }); 199 | } 200 | } 201 | 202 | @NgModule({ 203 | declarations: [TwitterComponent], 204 | exports: [TwitterComponent], 205 | imports: [CommonModule, SwatchModule, EditableInputModule], 206 | }) 207 | export class ColorTwitterModule {} 208 | -------------------------------------------------------------------------------- /src/lib/twitter/twitter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { By } from '@angular/platform-browser'; 4 | 5 | import { ColorTwitterModule } from './twitter.component'; 6 | 7 | describe('TwitterComponent', () => { 8 | beforeEach(waitForAsync(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [TwitterTestApp], 11 | imports: [ColorTwitterModule], 12 | }).compileComponents(); 13 | })); 14 | it(`should apply className to root element`, () => { 15 | const fixture = TestBed.createComponent(TwitterTestApp); 16 | fixture.detectChanges(); 17 | const divDebugElement = fixture.debugElement.query(By.css('.twitter-picker')); 18 | expect(divDebugElement.nativeElement.classList.contains('classy')).toBe(true); 19 | }); 20 | }); 21 | 22 | @Component({ 23 | selector: 'test-app', 24 | template: ``, 25 | standalone: false, 26 | }) 27 | class TwitterTestApp { 28 | className = 'classy'; 29 | } 30 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch(err => console.log(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'bootstrap/dist/css/bootstrap.css'; 2 | 3 | html { 4 | touch-action: manipulation; 5 | } 6 | html, 7 | body { 8 | background: #eee; 9 | } 10 | .home { 11 | font-family: 'Roboto', sans-serif; 12 | } 13 | 14 | /* fix weird issue on demo with background overlaying */ 15 | .col-md-6, 16 | .col-md-3, 17 | .col-md-4, 18 | .col-md-8 { 19 | z-index: 3; 20 | } 21 | 22 | .container { 23 | max-width: 800px; 24 | } 25 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | import 'zone.js/testing'; 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { 5 | BrowserDynamicTestingModule, 6 | platformBrowserDynamicTesting, 7 | } from '@angular/platform-browser-dynamic/testing'; 8 | 9 | // First, initialize the Angular testing environment. 10 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { 11 | teardown: { destroyAfterEach: false }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'material-colors'; 2 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitAny": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "lib": ["es2022", "dom"], 21 | "skipLibCheck": true, 22 | "paths": { 23 | "ngx-color": ["./src/lib/public_api"] 24 | }, 25 | "useDefineForClassFields": false 26 | }, 27 | "angularCompilerOptions": { 28 | "strictInjectionParameters": true, 29 | "strictTemplates": true, 30 | "enableI18nLegacyMessageIdFormat": false, 31 | "strictInputAccessModifiers": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "files": ["src/test.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------