diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..44349ca
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,15 @@
+# These are supported funding model platforms
+
+github: [sha-wrks]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+polar: # Replace with a single Polar username
+buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
+thanks_dev: # Replace with a single thanks.dev username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..effa76b
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,25 @@
+name: Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+ bundler-cache: true
+
+ - name: Run tests
+ run: bundle exec rspec
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d56e2c9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.gem
+*.rbc
+/.bundle/
+/coverage/
+/pkg/
+/spec/reports/
+/spec/examples.txt
+/tmp/
+Gemfile.lock
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..ff493aa
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+- Demonstrating empathy and kindness toward other people
+- Being respectful of differing opinions, viewpoints, and experiences
+- Giving and gracefully accepting constructive feedback
+- Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+- Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+- The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+- Trolling, insulting or derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+- Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+yansha@yansha.dev. All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..1300f7d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+# Contribution Guide
+
+Thank you for your interest in contributing to this project! Your contributions can help make this project better. Here are some guidelines to help you get started.
+
+## How to Contribute
+
+1. Fork this repository:
+ - Click the `Fork` button at the top of this repository page.
+2. Clone the forked repository to your local machine.
+ ```bash
+ git clone https://github.com/username/Ruby-Chromatic.git
+ ```
+ - Replace `username` with your GitHub username.
+3. Create a new branch
+ - Create a new branch for the feature or fix you want to add.
+ ```
+ cd Ruby-Chromatic
+ git checkout -b your-branch-name
+ ```
+ - Use a descriptive branch name that reflects the feature or fix you are working on.
+4. Make the necessary changes:
+ - Add or modify your code.
+5. Commit your changes:
+ - Make sure to write a clear and descriptive commit message.
+ ```
+ git add .
+ git commit -m "Brief description of the changes you made"
+ ```
+6. Push to your repository:
+ - Push your branch to your GitHub repository.
+ ```
+ git push origin your-branch-name
+ ```
+7. Create a Pull Request (PR):
+ - Go to the original repository page and create a pull request from your branch.
+ - Provide a clear description of what you added or fixed in your pull request.
+
+## Coding Guidelines
+
+- Follow a consistent coding standard: Ensure your code is consistent with the existing code style in this project.
+- Write clear documentation: Add necessary comments and documentation to help others understand your code.
+- Write tests: If possible, add tests for the features or fixes you are adding.
+
+## Reporting Issues
+
+If you find any bugs or have suggestions for improvements, please create a new issue on the Issues page.
+
+## Communication
+
+If you want to discuss something related to your contribution or the project in general, feel free to reach out to us via [Discussions](https://github.com/sha-wrks/Ruby-Chromatic/discussions).
+
+
+Thank you for your contribution!
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..fa75df1
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+source 'https://rubygems.org'
+
+gemspec
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..410d926
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Yansha
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c6a6d3f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,128 @@
+
+
+# Ruby Chromatic
+
+[](https://github.com/sha-wrks/Ruby-Chromatic/actions/workflows/tests.yml)
+[](https://www.ruby-lang.org)
+[](LICENSE)
+
+A Ruby gem for generating harmonious color palettes from a single base color using color theory algorithms. Works as both a CLI tool and a Ruby library.
+
+
+
+---
+
+## Features
+
+- Generate palettes using **5 color theory modes**: complementary, analogous, triadic, tetradic, and shades
+- Output in **3 formats**: plain text, CSS custom properties, and JSON
+- Full **HEX, RGB, and HSL** color conversion
+- Usable as a **CLI tool** or embedded as a **Ruby library**
+
+## Installation
+
+Add to your `Gemfile`:
+
+```ruby
+gem 'ruby-chromatic'
+```
+
+Then run:
+
+```sh
+bundle install
+```
+
+Or install directly:
+
+```sh
+gem install ruby-chromatic
+```
+
+## Usage
+
+### CLI
+
+```sh
+# Generate a complementary palette (default)
+chromatic generate "#3498db"
+
+# Specify a mode
+chromatic generate "#3498db" triadic
+
+# Output as CSS custom properties
+chromatic generate "#3498db" triadic --format css
+
+# Output as JSON with 7 shades
+chromatic generate "#e74c3c" shades --format json --steps 7
+
+# Show version
+chromatic version
+```
+
+**Available modes:**
+
+| Mode | Description |
+|---|---|
+| `complementary` | 2 colors opposite on the color wheel |
+| `analogous` | 3 colors adjacent on the color wheel |
+| `triadic` | 3 colors evenly spaced at 120 degrees |
+| `tetradic` | 4 colors evenly spaced at 90 degrees |
+| `shades` | Lightness gradient from dark to light |
+
+**Available formats:** `text` (default), `css`, `json`
+
+### Ruby Library
+
+```ruby
+require 'chromatic'
+
+# Create a color
+color = Chromatic::Color.from_hex('#3498db')
+
+# Convert between formats
+color.to_hex # => "#3498db"
+color.to_rgb # => "rgb(52, 152, 219)"
+color.to_hsl # => { h: 204.1, s: 69.9, l: 53.1 }
+
+# Generate palettes
+palette = Chromatic::Generator.complementary(color)
+palette = Chromatic::Generator.triadic(color)
+palette = Chromatic::Generator.shades(color, steps: 7)
+
+# Format output
+puts Chromatic::Formatter.css(palette)
+puts Chromatic::Formatter.json(palette)
+puts Chromatic::Formatter.text(palette)
+```
+
+**Example CSS output:**
+
+```css
+:root {
+ --triadic-1: #3498db;
+ --triadic-2: #db3498;
+ --triadic-3: #98db34;
+}
+```
+
+## Development
+
+```sh
+git clone https://github.com/sha-wrks/Ruby-Chromatic.git
+cd Ruby-Chromatic
+bundle install
+bundle exec rspec
+```
+
+## Contributing
+
+Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.
+
+## Security
+
+To report a vulnerability, see [SECURITY.md](SECURITY.md).
+
+## License
+
+Released under the [MIT License](LICENSE). Copyright (c) 2026 Yansha.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..f34a48c
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,5 @@
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: :spec
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..2896ff7
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,38 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability within this repository, please follow these steps:
+
+1. **Do not disclose the vulnerability publicly.**
+
+ - Publicly disclosing a vulnerability can put users at risk. Please refrain from sharing the details of the vulnerability in public forums, issue trackers, or any other public channels.
+
+2. **Send an email to the security team.**
+
+ - Please report the vulnerability to our security team via email at [yansha@yansha.dev]. Include as much detail as possible about the vulnerability and any potential fixes.
+
+3. **Provide detailed information.**
+
+ - When reporting the vulnerability, please include the following information:
+ - A description of the vulnerability and its impact.
+ - Steps to reproduce the vulnerability.
+ - Any potential fixes or suggestions for mitigation.
+
+4. **Expect a response within 48 hours.**
+
+ - We will acknowledge receipt of your report within 48 hours and will provide a detailed response within 7 days, including an estimate for when a fix will be implemented.
+
+5. **Receive recognition for your contribution.**
+ - Once the vulnerability is resolved, we will credit you for your discovery in the release notes, unless you prefer to remain anonymous.
+
+## Security Update Policy
+
+We will issue security updates for supported versions of our project as soon as possible after a vulnerability is reported and verified. Our goal is to ensure the safety and security of our users at all times.
+
+---
+
+By following this Security Policy, you help us maintain a secure and reliable project for everyone. Thank you for your cooperation and contribution to the security of our project.
+
+If you have any questions or need further assistance, please contact us at [yansha@yansha.dev].
+
diff --git a/bin/chromatic b/bin/chromatic
new file mode 100644
index 0000000..a6cf9b8
--- /dev/null
+++ b/bin/chromatic
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
+
+require 'chromatic'
+
+Chromatic::CLI.start(ARGV)
diff --git a/chromatic.gemspec b/chromatic.gemspec
new file mode 100644
index 0000000..5b5649b
--- /dev/null
+++ b/chromatic.gemspec
@@ -0,0 +1,25 @@
+require_relative 'lib/chromatic/version'
+
+Gem::Specification.new do |spec|
+ spec.name = 'ruby-chromatic'
+ spec.version = Chromatic::VERSION
+ spec.authors = ['sha-wrks']
+ spec.email = ['yanshaaa28@gmail.com']
+
+ spec.summary = 'Color palette generator for Ruby'
+ spec.description = 'Generate beautiful color palettes using color theory - complementary, analogous, triadic, and more.'
+ spec.homepage = 'https://github.com/sha-wrks/Ruby-Chromatic'
+ spec.license = 'MIT'
+
+ spec.required_ruby_version = '>= 3.0.0'
+
+ spec.files = Dir['lib/**/*', 'bin/*']
+ spec.bindir = 'bin'
+ spec.executables = ['chromatic']
+ spec.require_paths = ['lib']
+
+ spec.add_dependency 'thor', '~> 1.0'
+
+ spec.add_development_dependency 'rake', '~> 13.0'
+ spec.add_development_dependency 'rspec', '~> 3.0'
+end
diff --git a/lib/chromatic.rb b/lib/chromatic.rb
new file mode 100644
index 0000000..197b43b
--- /dev/null
+++ b/lib/chromatic.rb
@@ -0,0 +1,9 @@
+require_relative 'chromatic/version'
+require_relative 'chromatic/color'
+require_relative 'chromatic/palette'
+require_relative 'chromatic/generator'
+require_relative 'chromatic/formatter'
+require_relative 'chromatic/cli'
+
+module Chromatic
+end
diff --git a/lib/chromatic/cli.rb b/lib/chromatic/cli.rb
new file mode 100644
index 0000000..50f1066
--- /dev/null
+++ b/lib/chromatic/cli.rb
@@ -0,0 +1,36 @@
+require 'thor'
+
+module Chromatic
+ class CLI < Thor
+ desc 'generate HEX MODE', 'Generate a palette from a hex color'
+ option :format, aliases: '-f', default: 'text', desc: 'Output format: text, css, json'
+ option :steps, aliases: '-s', type: :numeric, default: 5, desc: 'Steps for shades mode'
+ def generate(hex, mode = 'complementary')
+ color = Color.from_hex(hex)
+
+ palette = case mode
+ when 'complementary' then Generator.complementary(color)
+ when 'analogous' then Generator.analogous(color)
+ when 'triadic' then Generator.triadic(color)
+ when 'tetradic' then Generator.tetradic(color)
+ when 'shades' then Generator.shades(color, steps: options[:steps])
+ else
+ warn "Unknown mode: #{mode}"
+ exit 1
+ end
+
+ output = case options[:format]
+ when 'css' then Formatter.css(palette)
+ when 'json' then Formatter.json(palette)
+ else Formatter.text(palette)
+ end
+
+ puts output
+ end
+
+ desc 'version', 'Show version'
+ def version
+ puts "chromatic #{VERSION}"
+ end
+ end
+end
diff --git a/lib/chromatic/color.rb b/lib/chromatic/color.rb
new file mode 100644
index 0000000..8d10614
--- /dev/null
+++ b/lib/chromatic/color.rb
@@ -0,0 +1,74 @@
+module Chromatic
+ class Color
+ attr_reader :r, :g, :b
+
+ def initialize(r, g, b)
+ @r = r.clamp(0, 255)
+ @g = g.clamp(0, 255)
+ @b = b.clamp(0, 255)
+ end
+
+ def self.from_hex(hex)
+ hex = hex.delete_prefix('#')
+ r, g, b = hex.scan(/../).map { |c| c.to_i(16) }
+ new(r, g, b)
+ end
+
+ def self.from_hsl(h, s, l)
+ s /= 100.0
+ l /= 100.0
+
+ c = (1 - (2 * l - 1).abs) * s
+ x = c * (1 - ((h / 60.0) % 2 - 1).abs)
+ m = l - c / 2.0
+
+ r1, g1, b1 = case h
+ when 0...60 then [c, x, 0]
+ when 60...120 then [x, c, 0]
+ when 120...180 then [0, c, x]
+ when 180...240 then [0, x, c]
+ when 240...300 then [x, 0, c]
+ else [c, 0, x]
+ end
+
+ new(((r1 + m) * 255).round, ((g1 + m) * 255).round, ((b1 + m) * 255).round)
+ end
+
+ def to_hex
+ '#%02x%02x%02x' % [@r, @g, @b]
+ end
+
+ def to_rgb
+ "rgb(#{@r}, #{@g}, #{@b})"
+ end
+
+ def to_hsl
+ r = @r / 255.0
+ g = @g / 255.0
+ b = @b / 255.0
+
+ max = [r, g, b].max
+ min = [r, g, b].min
+ delta = max - min
+
+ l = (max + min) / 2.0
+ s = delta.zero? ? 0.0 : delta / (1 - (2 * l - 1).abs)
+
+ h = if delta.zero?
+ 0.0
+ elsif max == r
+ 60 * (((g - b) / delta) % 6)
+ elsif max == g
+ 60 * (((b - r) / delta) + 2)
+ else
+ 60 * (((r - g) / delta) + 4)
+ end
+
+ { h: h.round(1), s: (s * 100).round(1), l: (l * 100).round(1) }
+ end
+
+ def to_s
+ to_hex
+ end
+ end
+end
diff --git a/lib/chromatic/formatter.rb b/lib/chromatic/formatter.rb
new file mode 100644
index 0000000..f2bc80a
--- /dev/null
+++ b/lib/chromatic/formatter.rb
@@ -0,0 +1,32 @@
+require 'json'
+
+module Chromatic
+ class Formatter
+ def self.css(palette)
+ vars = palette.colors.each_with_index.map do |color, i|
+ " --#{palette.name}-#{i + 1}: #{color.to_hex};"
+ end
+ ":root {\n#{vars.join("\n")}\n}"
+ end
+
+ def self.json(palette)
+ data = {
+ name: palette.name,
+ colors: palette.colors.map do |color|
+ hsl = color.to_hsl
+ { hex: color.to_hex, rgb: color.to_rgb, hsl: "hsl(#{hsl[:h]}, #{hsl[:s]}%, #{hsl[:l]}%)" }
+ end
+ }
+ JSON.pretty_generate(data)
+ end
+
+ def self.text(palette)
+ lines = ["Palette: #{palette.name}", '-' * 30]
+ palette.colors.each_with_index do |color, i|
+ hsl = color.to_hsl
+ lines << "#{i + 1}. #{color.to_hex} #{color.to_rgb} hsl(#{hsl[:h]}, #{hsl[:s]}%, #{hsl[:l]}%)"
+ end
+ lines.join("\n")
+ end
+ end
+end
diff --git a/lib/chromatic/generator.rb b/lib/chromatic/generator.rb
new file mode 100644
index 0000000..835c5dd
--- /dev/null
+++ b/lib/chromatic/generator.rb
@@ -0,0 +1,42 @@
+module Chromatic
+ class Generator
+ def self.complementary(color)
+ hsl = color.to_hsl
+ complement = Color.from_hsl((hsl[:h] + 180) % 360, hsl[:s], hsl[:l])
+ Palette.new('complementary', [color, complement])
+ end
+
+ def self.analogous(color, spread: 30)
+ hsl = color.to_hsl
+ colors = [-spread, 0, spread].map do |offset|
+ Color.from_hsl((hsl[:h] + offset) % 360, hsl[:s], hsl[:l])
+ end
+ Palette.new('analogous', colors)
+ end
+
+ def self.triadic(color)
+ hsl = color.to_hsl
+ colors = [0, 120, 240].map do |offset|
+ Color.from_hsl((hsl[:h] + offset) % 360, hsl[:s], hsl[:l])
+ end
+ Palette.new('triadic', colors)
+ end
+
+ def self.tetradic(color)
+ hsl = color.to_hsl
+ colors = [0, 90, 180, 270].map do |offset|
+ Color.from_hsl((hsl[:h] + offset) % 360, hsl[:s], hsl[:l])
+ end
+ Palette.new('tetradic', colors)
+ end
+
+ def self.shades(color, steps: 5)
+ hsl = color.to_hsl
+ colors = steps.times.map do |i|
+ l = (i * 100.0 / (steps - 1)).round(1)
+ Color.from_hsl(hsl[:h], hsl[:s], l)
+ end
+ Palette.new('shades', colors)
+ end
+ end
+end
diff --git a/lib/chromatic/palette.rb b/lib/chromatic/palette.rb
new file mode 100644
index 0000000..dec5db0
--- /dev/null
+++ b/lib/chromatic/palette.rb
@@ -0,0 +1,35 @@
+module Chromatic
+ class Palette
+ attr_reader :name, :colors
+
+ def initialize(name, colors = [])
+ @name = name
+ @colors = colors
+ end
+
+ def add(color)
+ @colors << color
+ self
+ end
+
+ def size
+ @colors.size
+ end
+
+ def [](index)
+ @colors[index]
+ end
+
+ def each(&block)
+ @colors.each(&block)
+ end
+
+ def map(&block)
+ @colors.map(&block)
+ end
+
+ def to_s
+ "Palette(#{@name}): #{@colors.map(&:to_hex).join(', ')}"
+ end
+ end
+end
diff --git a/lib/chromatic/version.rb b/lib/chromatic/version.rb
new file mode 100644
index 0000000..4a9a906
--- /dev/null
+++ b/lib/chromatic/version.rb
@@ -0,0 +1,3 @@
+module Chromatic
+ VERSION = '0.1.0'
+end
diff --git a/spec/chromatic_spec.rb b/spec/chromatic_spec.rb
new file mode 100644
index 0000000..139334a
--- /dev/null
+++ b/spec/chromatic_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+RSpec.describe Chromatic do
+ it 'has a version number' do
+ expect(Chromatic::VERSION).not_to be_nil
+ end
+end
diff --git a/spec/formatter_spec.rb b/spec/formatter_spec.rb
new file mode 100644
index 0000000..f8fa610
--- /dev/null
+++ b/spec/formatter_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+RSpec.describe Chromatic::Formatter do
+ let(:color) { Chromatic::Color.from_hex('#3498db') }
+ let(:palette) { Chromatic::Generator.complementary(color) }
+
+ describe '.css' do
+ it 'outputs :root block with CSS variables' do
+ output = described_class.css(palette)
+ expect(output).to include(':root')
+ expect(output).to include('--complementary-1')
+ expect(output).to include('--complementary-2')
+ end
+ end
+
+ describe '.json' do
+ it 'outputs valid JSON with name and colors' do
+ output = described_class.json(palette)
+ data = JSON.parse(output)
+ expect(data['name']).to eq('complementary')
+ expect(data['colors'].size).to eq(2)
+ expect(data['colors'].first).to have_key('hex')
+ end
+ end
+
+ describe '.text' do
+ it 'includes palette name and hex values' do
+ output = described_class.text(palette)
+ expect(output).to include('complementary')
+ expect(output).to include('#')
+ end
+ end
+end
diff --git a/spec/palette_spec.rb b/spec/palette_spec.rb
new file mode 100644
index 0000000..f8312cc
--- /dev/null
+++ b/spec/palette_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+RSpec.describe Chromatic::Palette do
+ let(:red) { Chromatic::Color.from_hex('#ff0000') }
+ let(:blue) { Chromatic::Color.from_hex('#0000ff') }
+ subject(:palette) { described_class.new('test', [red, blue]) }
+
+ describe '#size' do
+ it 'returns the number of colors' do
+ expect(palette.size).to eq(2)
+ end
+ end
+
+ describe '#[]' do
+ it 'returns color by index' do
+ expect(palette[0].to_hex).to eq('#ff0000')
+ end
+ end
+
+ describe '#add' do
+ it 'appends a color and returns self' do
+ green = Chromatic::Color.from_hex('#00ff00')
+ result = palette.add(green)
+ expect(result).to be(palette)
+ expect(palette.size).to eq(3)
+ end
+ end
+end
+
+RSpec.describe Chromatic::Generator do
+ let(:color) { Chromatic::Color.from_hex('#3498db') }
+
+ describe '.complementary' do
+ it 'returns a palette with 2 colors' do
+ palette = described_class.complementary(color)
+ expect(palette.size).to eq(2)
+ end
+ end
+
+ describe '.triadic' do
+ it 'returns a palette with 3 colors' do
+ palette = described_class.triadic(color)
+ expect(palette.size).to eq(3)
+ end
+ end
+
+ describe '.shades' do
+ it 'returns a palette with the requested number of steps' do
+ palette = described_class.shades(color, steps: 7)
+ expect(palette.size).to eq(7)
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..af9964d
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,14 @@
+require 'chromatic'
+require 'json'
+
+RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.verify_partial_doubles = true
+ end
+
+ config.shared_context_metadata_behavior = :apply_to_host_groups
+end