├── .github ├── FUNDING.yml └── workflows │ ├── coverage.yml │ ├── lint.yml │ ├── release.yml │ └── verify.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bun.lockb ├── bunfig.toml ├── eslint.config.mjs ├── package.json ├── rollup.config.mjs ├── src ├── chroma.spec.ts └── chroma.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kwhitley 4 | open_collective: kevinrwhitley 5 | # patreon: # Replace with a single Patreon username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | on: ['push', 'pull_request_target'] 2 | 3 | name: Test Coveralls 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: oven-sh/setup-bun@v2.0.1 12 | - name: install, make test-coverage 13 | run: | 14 | bun install 15 | bun test --coverage 16 | - name: Coveralls 17 | uses: coverallsapp/github-action@master 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [v1.x] 6 | pull_request: 7 | branches: [v1.x] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: oven-sh/setup-bun@v2.0.1 16 | - name: Install dependencies 17 | - run: bun install 18 | - run: bun run lint 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | 15 | - name: Prepare tag for release notes 16 | id: prep 17 | run: echo "::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/} | sed 's/\.//g')" 18 | 19 | - name: Create Release 20 | uses: actions/create-release@v1 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: Release ${{ github.ref }} 26 | body: | 27 | See [${{ github.ref_name }} CHANGELOG entry](https://github.com/kwhitley/itty-chroma/blob/v0.x/CHANGELOG.md#${{ steps.prep.outputs.tag }}) for notes 28 | draft: false 29 | prerelease: false 30 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [v0.x] 6 | pull_request: 7 | branches: [v0.x] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: oven-sh/setup-bun@v2.0.1 16 | - name: Install dependencies 17 | - run: bun install 18 | - run: bun run verify 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS ignores 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | *.tmp 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Docs ignores 111 | docs/dist 112 | docs/dist-ssr 113 | docs/*.local 114 | docs/pages/README.md 115 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "all", 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | #### v1.0.0 4 | - initial public release, considered stable 5 | #### v0.1.18 6 | - final pre-release 7 | #### v0.1.17 8 | - semifinal README 9 | #### v0.1.16 10 | - updates to README 11 | #### v0.1.15 12 | - mic check... check, check 13 | #### v0.1.x 14 | - test release 15 | 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | krwhitley@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Itty 2 | 3 | The [Open Source Guides](https://opensource.guide/) website has a collection of resources for individuals, communities, and companies. These resources help people who want to learn how to run and contribute to open source projects. Contributors and people new to open source alike will find the following guides especially useful: 4 | 5 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 6 | - [Building Welcoming Communities](https://opensource.guide/building-community/) 7 | 8 | ## Bugs 9 | 10 | We use [GitHub issues](https://github.com/kwhitley/itty-router/issues) for our public bugs. If you would like to report a problem, take a look around and see if someone already opened an issue about it. If you are certain this is a new unreported bug, you can submit a [bug report](#reporting-new-issues). 11 | 12 | If you have questions about using itty, [contact us on Discord](https://discord.com/channels/832353585802903572), and we will do our best to answer your questions. 13 | 14 | ### Reporting new issues 15 | 16 | When [opening a new issue](https://github.com/kwhitley/itty-router/issues/new/choose), always make sure to fill out the issue template. **This step is very important!** Not doing so may result in your issue not being managed in a timely fashion. Don't take this personally if this happens, and feel free to open a new issue once you've gathered all the information required by the template. 17 | 18 | - **One issue, one bug:** Please report a single bug per issue. 19 | - **Provide reproduction steps:** List all the steps necessary to reproduce the issue. The person reading your bug report should be able to follow these steps to reproduce your issue with minimal effort. 20 | 21 | ### Proposing a change 22 | 23 | If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with [feature template](https://github.com/kwhitley/itty-router/issues/new?template=feature_request.yml). 24 | 25 | If you're only fixing a bug, it's fine to submit a pull request right away, but we still recommend that you file an issue detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. 26 | 27 | Small pull requests are much easier to review and more likely to get merged. 28 | 29 | ### Installation 30 | 31 | 1. Ensure you have [npm](https://www.npmjs.com/get-npm) installed. 32 | 1. Ensure you have [yarn](https://classic.yarnpkg.com/lang/en/docs/install) installed. 33 | 1. After cloning the repository, run `yarn` in the root of the repository. 34 | 1. To start development, run `yarn dev`. 35 | 36 | ### Creating a branch 37 | 38 | Fork [the repository](https://github.com/kwhitley/itty-router) and create your branch from `v4.x`. If you've never sent a GitHub pull request before, you can learn how from [this free video series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 39 | 40 | ### Testing 41 | 42 | A good test plan has the exact commands you ran and their output, provides screenshots or videos if the pull request changes UI. 43 | 44 | - If you've changed APIs, update the documentation, including at the appropriate places within [itty.dev](https://itty.dev/itty-router). 45 | 46 | #### Writing tests 47 | 48 | All tests are located in adjacent `.spec.ts` files, next to the file being tested. 49 | 50 | #### Running tests 51 | 52 | 1. To run test, run `yarn test`, or `yarn dev` for continuous testing (includes `--watch`). 53 | 54 | ### Style guide 55 | 56 | [Eslint](https://eslint.org) will catch most styling issues that may exist in your code. You can check the status of your code styling by simply running `yarn lint`. 57 | 58 | #### Code conventions 59 | 60 | - `camelCase` for public variable names and methods. 61 | - No abbreviated variable names - maximize readability and let the minification process do its thing later. 62 | 63 | ### Sending your pull request 64 | 65 | Please make sure the following is done when submitting a pull request: 66 | 67 | 1. Describe your **test plan** in your pull request description. Make sure to test your changes. 68 | 1. Make sure your code lints (`yarn lint`). 69 | 1. Make sure your tests pass (`yarn test`). 70 | 71 | All pull requests should be opened against the `v4.x` branch. Make sure the PR does only one thing, otherwise please split it. 72 | 73 | #### Breaking changes 74 | 75 | When adding a new breaking change, follow this template in your pull request: 76 | 77 | ```md 78 | ### New breaking change here 79 | 80 | - **Who does this affect**: 81 | - **How to migrate**: 82 | - **Why make this breaking change**: 83 | - **Severity (number of people affected x effort)**: 84 | ``` 85 | 86 | ## License 87 | 88 | By contributing to itty, you agree that your contributions will be licensed under its [MIT license](https://github.com/kwhitley/itty-router/blob/master/LICENSE). 89 | 90 | ## Questions 91 | 92 | Feel free to ask in [#itty-router](https://discord.com/channels/832353585802903572) on [Discord](https://discord.com/channels/832353585802903572) if you have questions about our process, how to proceed, etc. 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kevin R. Whitley 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | itty-chroma 6 | 7 |

8 | 9 | [![Version](https://img.shields.io/npm/v/itty-chroma.svg?style=flat-square)](https://npmjs.com/package/itty-chroma) 10 | [![Bundle Size](https://deno.bundlejs.com/?q=itty-chroma&badge&badge-style=flat-square)](https://deno.bundlejs.com/?q=itty-chroma) 11 | [![Coverage Status](https://img.shields.io/coveralls/github/kwhitley/itty-chroma?style=flat-square)](https://coveralls.io/github/kwhitley/itty-chroma) 12 | [![Issues](https://img.shields.io/github/issues/kwhitley/itty-chroma?style=flat-square)](https://coveralls.io/github/kwhitley/itty-chroma) 13 | [![Discord](https://img.shields.io/discord/832353585802903572?label=Discord&logo=Discord&style=flat-square&logoColor=fff)](https://discord.gg/53vyrZAu9u) 14 | 15 | ### [v1 Documentation](https://itty.dev/itty-chroma)  |   [Discord](https://discord.gg/53vyrZAu9u) 16 | 17 | --- 18 | 19 | Powerful styling for the browser console in under 500 bytes. 20 | 21 | 22 | ## Example 23 | ```ts 24 | import { chroma } from 'itty-chroma' 25 | 26 | // keep it simple 27 | chroma.red.log('This will be red.') 28 | 29 | // or play a little 30 | chroma.log( 31 | chroma.green, // set the color to green 32 | 'This is all', 33 | 'green.', 34 | chroma.blue.underline.bold, // now switch to blue 35 | 'Now this is blue.', 36 | ) 37 | ``` 38 | ![image](https://github.com/user-attachments/assets/7f84014b-97e1-474f-8020-3430efd3e0c6) 39 | 40 | ## Features 41 | 42 | - Tiny. It's an itty library, after all. 43 | - Made specifically for the browser console. 44 | - Loads of styling options, with infinite combinations. 45 | - Simple and powerful API, designed for ease & readability. 46 | 47 |
48 | 49 | # How it Works 50 | 51 | Chroma is an infinite proxy object/function chain... *thingy*... that assembles styles before sending them to `console.log`. 52 | 53 | This sounds very confusing... which is understandable, because it was confusing to write as well. 54 | 55 | Here are the basic rules: 56 | 57 | ### 1. Use `chroma.log` (also supports `warn` and `error`) to enable styling 58 | ```ts 59 | chroma.log('text') // console.log('text') 60 | chroma.warn('text') // console.warn('text') 61 | chroma.error('text') // console.error('text') 62 | ``` 63 | ![image](https://github.com/user-attachments/assets/0c82f3e9-0fae-4e4a-a021-6d334874ed00) 64 | 65 | 66 | ### 2. Add styles by chaining style properties 67 | ```ts 68 | // call a segment directly, using .log 69 | chroma.bold.red.log('This will be red.') 70 | ``` 71 | ![image](https://github.com/user-attachments/assets/63a78004-87f2-4bf2-ba9e-60407b986419) 72 | 73 | ### 3. Or compose using chroma segments 74 | ```ts 75 | chroma.log( 76 | chroma.bold.green, 77 | 'This will be green.' 78 | ) 79 | ``` 80 | ![image](https://github.com/user-attachments/assets/04a68ebd-3c46-45cc-ad71-9ed8e68b98fc) 81 | 82 | These can be saved for re-use: 83 | ```ts 84 | const blue = chroma.bold.blue 85 | 86 | chroma.log( 87 | blue, 88 | 'This will be blue.' 89 | ) 90 | ``` 91 | ![image](https://github.com/user-attachments/assets/d1083073-f33d-4356-8b21-37ae02fe0d3c) 92 | 93 | They can also be saved with the `.log` as a custom logger: 94 | ```ts 95 | const ittyLogger = chroma.bold.color("#f0c").log 96 | 97 | ittyLogger('This will be itty-colored.') 98 | ``` 99 | ![image](https://github.com/user-attachments/assets/0a2e05aa-923c-4d47-98b8-bf3f583a3cf4) 100 | 101 | ### 4. Any valid CSS color name works (100% support) 102 | ```ts 103 | chroma.salmon.log('This is salmon.') 104 | chroma.cornflowerblue.log('This is cornflowerblue.') 105 | chroma.cornFlowerBlue.log('Case does not matter here...') 106 | ``` 107 | ![image](https://github.com/user-attachments/assets/b363fcec-a289-4f25-af8c-d3d5f31e532f) 108 | 109 | ### 5. All valid CSS works within properties that expect a value 110 | ```ts 111 | chroma 112 | .color('rgba(255,0,100,0.4)') 113 | .log('This works just fine') 114 | ``` 115 | ![image](https://github.com/user-attachments/assets/98f978a0-87b6-4488-8f22-696452e927d0) 116 | 117 | ### 6. ...or use your own custom CSS with `.style(css: string)` 118 | ```ts 119 | chroma 120 | .size('2em') 121 | .padding('0.5em') 122 | .style('text-transform:uppercase; text-shadow:0 0 0.5em magenta;') 123 | .log('So does this') 124 | ``` 125 | ![image](https://github.com/user-attachments/assets/3a6e5bcf-99ab-4616-9794-579c2e0e6cc8) 126 | 127 | ### 7. A style will continue until replaced, or cleared using **`chroma.none`** 128 | ```ts 129 | chroma.log( 130 | chroma.red('this will be red'), 131 | '...but so will this', 132 | chroma.none, 133 | 'back to unformatted text' 134 | ) 135 | ``` 136 | ![image](https://github.com/user-attachments/assets/d970e8c1-1249-4a39-a183-845ccd5d841f) 137 | 138 | ### 8. Example: Creating custom log functions 139 | ```ts 140 | // we define a curried function to accept some args now, some later 141 | const createLogger = (type = 'log', label, badge = 'grey', text = 'grey') => 142 | (...args) => 143 | chroma[type]( 144 | chroma.bg(badge).white.bold.padding('2px 5px 1px').radius('0.2rem')(label), 145 | chroma.color(text).italic, 146 | ...args, 147 | ) 148 | 149 | // our loggers are partial executions 150 | const info = createLogger('log', 'INFO', 'green') 151 | const warning = createLogger('warn', 'WARNING', 'orange', 'brown') 152 | 153 | // and we finally call them to log messages 154 | info('This is just a helpful bit of info!') 155 | warning('But this is a more serious warning text...') 156 | ``` 157 | ![image](https://github.com/user-attachments/assets/58cdbcbb-51c3-4b67-8fe8-323bf3a094cd) 158 | 159 |
160 | 161 | # Supported Properties 162 | 163 | | PROPERTY | DESCRIPTION | EXAMPLE(s) | 164 | | --- | --- | --- | 165 | | **.log** | once executed, will output to console.log | `chroma.log('hello')` | 166 | | **.warn** | once executed, will output to console.warn | `chroma.warn('warning text')` | 167 | | **.error** | once executed, will output to console.error | `chroma.error('error text')` | 168 | | **.bold** | bold text | `chroma.bold('this is bold')`, `chroma.bold.red('this is bold and red')` | 169 | | **.italic** | italicized text | `chroma.italic('this is italic')` | 170 | | **.underline** | underlined text | `chroma.underline('text')` | 171 | | **.strike** | text with a line through it | `chroma.strike('this text was removed')` | 172 | | **.{colorName}** | sets text color, supports any valid CSS color name | `chroma.magenta`, `chroma.lightGreen` | 173 | | **.color(value)** | sets font color, supports any valid CSS color | `chroma.color('white')`, `chroma.color('rgba(255,0,0,0.2)')` | 174 | | **.font(value)** | sets font, supports any valid CSS font-family | `chroma.font('Georgia')` | 175 | | **.size(value)** | sets font size | `chroma.size('0.8rem')` | 176 | | **.bg(value)** | sets background, supports any valid CSS background | `chroma.bg('salmon')` | 177 | | **.radius(value)** | sets border-radius (for badges) | `chroma.radius('4px')` | 178 | | **.border(value)** | sets border style | `chroma.border('double 5px red')` | 179 | | **.padding(value)** | sets padding | `chroma.padding('2px 5px')` | 180 | | **.style(value)** | sets custom CSS, allowing any valid sequence | `chroma.style('text-transform:uppercase;text-shadow:0 0 0.5rem rgba(255,0,100,0.5)')` | 181 | | **.none**1 | clears styling for subsequent arguments | `chroma.red('red text', chroma.none, 'plain text')` | 182 | 183 | 1 Any invalid CSS color name can be used in place of **chroma.none**, as this utimately turns into `"color:none;"`. Alternatively, you could use **chroma.clear**, **chroma.noStyle**, or anything else. 184 | 185 | 186 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwhitley/itty-chroma/44ae059177818ec3d829bbd60274e6a2bad32829/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | coverage = true 3 | coverageReporter = ["text", "lcov"] -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin' 2 | import tsParser from '@typescript-eslint/parser' 3 | import path from 'node:path' 4 | import { fileURLToPath } from 'node:url' 5 | import js from '@eslint/js' 6 | import { FlatCompat } from '@eslint/eslintrc' 7 | 8 | const __filename = fileURLToPath(import.meta.url) 9 | const __dirname = path.dirname(__filename) 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all 14 | }) 15 | 16 | export default [...compat.extends( 17 | 'eslint:recommended', 18 | 'plugin:@typescript-eslint/eslint-recommended', 19 | 'plugin:@typescript-eslint/recommended', 20 | ), { 21 | plugins: { 22 | '@typescript-eslint': typescriptEslint, 23 | }, 24 | 25 | languageOptions: { 26 | parser: tsParser, 27 | }, 28 | 29 | rules: { 30 | '@typescript-eslint/no-empty-function': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | '@typescript-eslint/ban-types': 'off', 33 | '@typescript-eslint/ban-ts-comment': 'off', 34 | 'linebreak-style': ['error', 'unix'], 35 | 'prefer-const': 'off', 36 | 37 | quotes: ['error', 'single', { 38 | allowTemplateLiterals: true, 39 | }], 40 | 41 | semi: ['error', 'never'], 42 | }, 43 | }] -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itty-chroma", 3 | "version": "1.0.0", 4 | "description": "Powerful styling for the browser console in under 500 bytes.", 5 | "main": "./chroma.js", 6 | "module": "./chroma.mjs", 7 | "types": "./chroma.d.ts", 8 | "scripts": { 9 | "dev": "bun test --watch", 10 | "lint": "bun eslint src", 11 | "verify": "echo 'verifying module...' && bun run build && bun test", 12 | "prerelease": "bun run verify", 13 | "prerelease:next": "bun run verify", 14 | "prebuild": "rimraf dist && mkdir dist", 15 | "build": "rollup -c", 16 | "release": "release --tag --push --patch --src=dist", 17 | "release:next": "release --tag --push --type=next --src=dist" 18 | }, 19 | "keywords": [ 20 | "browser", 21 | "console", 22 | "logs", 23 | "color", 24 | "styles", 25 | "minimalist" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/kwhitley/itty-chroma.git" 30 | }, 31 | "author": "Kevin R. Whitley ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/kwhitley/itty-chroma/issues" 35 | }, 36 | "devDependencies": { 37 | "@rollup/plugin-terser": "^0.4.4", 38 | "@rollup/plugin-typescript": "^11.1.6", 39 | "@typescript-eslint/eslint-plugin": "^8.18.0", 40 | "@typescript-eslint/parser": "^8.18.0", 41 | "eslint": "^9.17.0", 42 | "rimraf": "^6.0.1", 43 | "rollup": "^4.28.1", 44 | "rollup-plugin-bundle-size": "^1.0.3", 45 | "rollup-plugin-copy": "^3.5.0", 46 | "typescript": "^5.7.2", 47 | "yarn-release": "^1.10.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import terser from '@rollup/plugin-terser' 2 | import typescript from '@rollup/plugin-typescript' 3 | import bundleSize from 'rollup-plugin-bundle-size' 4 | import copy from 'rollup-plugin-copy' 5 | 6 | export default { 7 | input: './src/chroma.ts', 8 | output: [ 9 | { 10 | format: 'esm', 11 | file: './dist/chroma.mjs', 12 | sourcemap: false, 13 | }, 14 | { 15 | format: 'cjs', 16 | file: './dist/chroma.js', 17 | sourcemap: false, 18 | }, 19 | ], 20 | plugins: [ 21 | typescript({ sourceMap: false }), 22 | terser(), 23 | bundleSize(), 24 | copy({ 25 | targets: [ 26 | { 27 | src: ['LICENSE'], 28 | dest: 'dist', 29 | }, 30 | ], 31 | }), 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /src/chroma.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, spyOn, mock } from 'bun:test' 2 | import { chroma } from './chroma' 3 | 4 | const isChroma = (instance: any) => typeof instance.a.b.c === 'function' 5 | 6 | describe('chroma', () => { 7 | it('is a function', () => { 8 | expect(typeof chroma).toBe('function') 9 | }) 10 | 11 | it('returns the the chroma Proxy', () => { 12 | expect(isChroma(chroma)).toBe(true) 13 | }) 14 | 15 | it('expty execution returns nothing', () => { 16 | expect(typeof chroma()).toBe('undefined') 17 | }) 18 | 19 | it('returns an infinite Proxy chain', () => { 20 | expect(typeof chroma.a.b.c.d.e.f).toBe('function') 21 | }) 22 | 23 | it('executing any node returns an array (unless log type specified)', () => { 24 | expect(Array.isArray(chroma.a.b.c('xyz'))).toBe(true) 25 | }) 26 | it('executing any node logs if log type specified', () => { 27 | const log = spyOn(console, 'log').mockImplementation(() => {}) 28 | const result = chroma.a.b.log.d('xyz') 29 | expect(Array.isArray(result)).toBe(false) 30 | expect(result).toBe(undefined) 31 | expect(log).toHaveBeenCalledTimes(1) 32 | log.mockRestore() 33 | }) 34 | 35 | describe('output support', () => { 36 | const types = ['log', 'warn', 'error'] 37 | 38 | for (const type of types) { 39 | const log = spyOn(console, type).mockImplementation(() => {}) 40 | it(`.${type}`, () => { 41 | chroma[type]('hey') 42 | expect(log).toHaveBeenCalledTimes(1) 43 | log.mockRestore() 44 | }) 45 | } 46 | }) 47 | 48 | describe('style support', () => { 49 | type Feature = [ 50 | property: string, 51 | match: string, 52 | argument?: string | number, 53 | ] 54 | const features: Array = [ 55 | ['color', 'color', '#aaa'], 56 | ['bold', 'font-weight:bold'], 57 | ['italic', 'font-style:italic'], 58 | ['underline', 'text-decoration:underline'], 59 | ['strike', 'text-decoration:line-through'], 60 | ['font', 'font-family', 'Georgia'], 61 | ['size', 'font-size', '0.9em'], 62 | ['bg', 'background', 'rgba(255,0,0,0.3)'], 63 | ['radius', 'border-radius', '3px'], 64 | ['padding', 'padding', '#aaa'], 65 | ['border', 'border', '1px solid red'], 66 | ] 67 | 68 | for (const [property, match, argument] of features) { 69 | it(argument ? `.${property}("${argument}")` : `.${property}`, () => { 70 | // @ts-ignore 71 | const out = argument ? chroma[property](argument)() : chroma[property]() 72 | const fullMatch = argument ? `${match}:${argument}` : match 73 | expect(out.join(' ').indexOf(fullMatch)).not.toBe(-1) 74 | }) 75 | } 76 | const styleString = 'text-transform:uppercase;margin-bottom:3rem' 77 | 78 | it(`.style("${styleString}")`, () => { 79 | const out = chroma.style(styleString)() 80 | expect(out.join(' ').indexOf(styleString)).not.toBe(-1) 81 | }) 82 | }) 83 | 84 | describe('barrier detection', () => { 85 | it('includes no color clear by default', () => { 86 | const result = chroma.red( 87 | 'red text', 88 | chroma.blue('blue text'), 89 | ) 90 | expect(result[0].indexOf('%c%s ')).toBe(0) 91 | expect(result.indexOf('')).toBe(-1) 92 | }) 93 | 94 | it('includes a barrier after padding', () => { 95 | const result = chroma.padding('5px')( 96 | 'padded text', 97 | chroma.blue('blue text'), 98 | ) 99 | expect(result[0].indexOf('%c%s%c ')).toBe(0) 100 | expect(result.indexOf('')).toBe(3) 101 | }) 102 | 103 | it('includes a barrier after text-decoration', () => { 104 | const result = chroma.red( 105 | 'padded text', 106 | chroma.strike, 107 | 'strike text', 108 | chroma.blue, 109 | 'blue text', 110 | ) 111 | expect(result[0]).toBe('%c%s %c%s%c %c%s ') 112 | expect(result.indexOf('')).toBe(5) 113 | }) 114 | }) 115 | 116 | describe('behavior', () => { 117 | it('will not execute non-chroma functions in the arguments', () => { 118 | const log = spyOn(console, 'log').mockImplementation(() => {}) 119 | const fn = mock(() => {}) 120 | chroma.red.log('hello', fn, 'world') 121 | 122 | expect(fn).not.toHaveBeenCalled() 123 | log.mockRestore() 124 | }) 125 | }) 126 | }) 127 | -------------------------------------------------------------------------------- /src/chroma.ts: -------------------------------------------------------------------------------- 1 | type StyleFunction = (value: T) => ColoredProxy 2 | 3 | type OutputFunction = (...args: Array) => Array 4 | 5 | type StyleMethods = { 6 | bold: ColoredProxy 7 | italic: ColoredProxy 8 | underline: ColoredProxy 9 | strikethrough: ColoredProxy 10 | font: StyleFunction 11 | size: StyleFunction 12 | bg: StyleFunction 13 | radius: StyleFunction 14 | color: StyleFunction 15 | padding: StyleFunction 16 | border: StyleFunction 17 | style: StyleFunction 18 | log: ColoredProxy 19 | warn: ColoredProxy 20 | error: ColoredProxy 21 | } 22 | 23 | type ColoredProxy = { 24 | [key: string]: ColoredProxy // Allows dynamic colors or methods like "red", "green", etc. 25 | } & StyleMethods & OutputFunction 26 | 27 | // @ts-ignore 28 | export const chroma: ColoredProxy = new Proxy(() => {}, { 29 | // @ts-ignore 30 | get( 31 | _: any, 32 | prop: string, 33 | __: any, 34 | styles: string = '', 35 | which: string | undefined, 36 | ) { 37 | return new Proxy( 38 | // @ts-ignore 39 | (...args: any[]) => { 40 | let out = [styles] 41 | let base = '%c' 42 | let wasPadded: any = styles.match(/pad|dec/) 43 | let isPadded 44 | 45 | for (let a of args) { 46 | if (a?.zq) a = a() // any chroma functions should be executed first 47 | if (a?.[0]?.startsWith?.('%c')) { 48 | isPadded = a[1].match(/pad|dec/) 49 | if (wasPadded) { 50 | base = base.slice(0, -1) 51 | } 52 | if (wasPadded && !isPadded) { 53 | base += '%c ' 54 | out.push('') 55 | } 56 | base += a[0] 57 | out.push(...a.slice(1)) 58 | wasPadded = isPadded 59 | } else { 60 | base += typeof a == 'object' ? '%o ' : '%s ' 61 | out.push(a) 62 | } 63 | } 64 | 65 | return which 66 | // @ts-ignore 67 | ? console[which](base.trim(), ...out) 68 | : [base, ...out] 69 | }, 70 | { 71 | get( 72 | _: any, 73 | prop: string, 74 | __: any, 75 | add = (type: string) => 76 | (value: string) => 77 | // (styles += `${type}${type ? ':' : ''}${value};`) && __, 78 | (styles += (type ? `${type}:${value}` : value) + ';') && __, 79 | ) { 80 | if (prop == 'color') return add(prop) 81 | if (prop == 'bold') return add('font-weight')(prop) 82 | if (prop == 'italic') return add('font-style')(prop) 83 | if (prop == 'underline') return add('text-decoration')(prop) 84 | if (prop == 'strike') return add('text-decoration')('line-through') 85 | if (prop == 'font') return add('font-family') 86 | if (prop == 'size') return add('font-size') 87 | if (prop == 'bg') return add('background') 88 | if (prop == 'radius') return add('border-radius') 89 | if (prop == 'padding') return add(prop) 90 | if (prop == 'border') return add(prop) 91 | if (prop == 'style') return add('') 92 | if (prop == 'log') return (which = prop) && __ 93 | if (prop == 'warn') return (which = prop) && __ 94 | if (prop == 'error') return (which = prop) && __ 95 | 96 | return add('color')(prop) 97 | }, 98 | })[prop] 99 | } 100 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "src", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "inlineSourceMap": false, 10 | "lib": ["esnext"], 11 | "listEmittedFiles": false, 12 | "listFiles": false, 13 | "noFallthroughCasesInSwitch": true, 14 | "pretty": true, 15 | // "moduleResolution": "nodeNext", // disabled to be compatible with module: "esnext" 16 | // "resolveJsonModule": true, // disabled to be compatible with module: "esnext" 17 | "rootDir": "src", 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "traceResolution": false, 21 | "outDir": "", 22 | "target": "esnext", 23 | "module": "esnext" 24 | }, 25 | "exclude": ["node_modules", "dist", "**/*.spec.ts", "examples"], 26 | "include": ["src", "src/seconds.ts"] 27 | } 28 | --------------------------------------------------------------------------------