\]./()*\\\n\t\b\v\u00A0]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),A=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||A||!1}}});
9 | // @license-end
--------------------------------------------------------------------------------
/src/styles/_dark.scss:
--------------------------------------------------------------------------------
1 | @use 'variables' as *;
2 |
3 | html.darkmode-active {
4 | background-color: $gray-600;
5 | color-scheme: dark;
6 |
7 | body {
8 | color: $gray-100;
9 | background-color: $gray-600;
10 |
11 | a {
12 | color: $gray-100;
13 | border-color: $gray-100;
14 | }
15 |
16 | .header {
17 | .preface {
18 | .theme-toggle {
19 | background-color: $black-transparent;
20 |
21 | &:hover {
22 | background-color: $black;
23 | }
24 |
25 | &:active {
26 | background-color: color-mix(in srgb, $black 85%, $white);
27 | }
28 |
29 | &-icon {
30 |
31 | svg {
32 | color: $white;
33 | }
34 |
35 | .icon-tabler-sun-high {
36 | display: inline-block;
37 | }
38 |
39 | .icon-tabler-moon {
40 | display: none;
41 | }
42 | }
43 | }
44 |
45 | .announcement {
46 | background-color: $black-transparent;
47 | color: $gray-100;
48 | border-color: $black-transparent;
49 |
50 | &:hover {
51 | background-color: $black;
52 | }
53 |
54 | &:active {
55 | background-color: color-mix(in srgb, $black 85%, $white);
56 | }
57 | }
58 |
59 | a.github-corner {
60 | fill: $black-transparent;
61 | }
62 | }
63 |
64 | .title a {
65 | color: $gray-50;
66 | }
67 | }
68 |
69 | .form {
70 | textarea {
71 | background-color: $gray-500;
72 | border-color: $gray-500;
73 | color: $gray-100;
74 | }
75 |
76 | .color-picker-button {
77 | background-color: $black-transparent;
78 | color: $gray-100;
79 | }
80 | }
81 |
82 | [data-tooltip]::after {
83 | background-color: $gray-500;
84 | color: $white;
85 | }
86 |
87 | #clr-picker {
88 | background-color: $black;
89 |
90 | input.clr-color {
91 | background-color: $gray-500;
92 | border-color: $gray-500;
93 | }
94 | }
95 |
96 | .palettes {
97 | .palette-controls {
98 | .step-selector-option {
99 | background-color: $black-transparent;
100 | color: $gray-100;
101 | border: none;
102 |
103 | &.is-active {
104 | box-shadow: inset 0 0 0 2px $gray-500;
105 | opacity: 1;
106 | background-color: $black;
107 |
108 | &:active {
109 | background-color: color-mix(in srgb, $black 85%, $white);
110 | }
111 | }
112 |
113 | &:hover {
114 | background-color: $black;
115 | }
116 |
117 | &:active {
118 | background-color: color-mix(in srgb, $black 85%, $white);
119 | }
120 | }
121 |
122 | .action-button {
123 | background-color: $black-transparent;
124 | color: $gray-100;
125 |
126 | &:hover {
127 | background-color: $black;
128 | }
129 |
130 | &:active {
131 | background-color: color-mix(in srgb, $black 85%, $white);
132 | }
133 |
134 | &.is-active {
135 | border-color: $gray-400;
136 | box-shadow: inset 0 0 0 2px $gray-500;
137 | background-color: $black;
138 |
139 | &:active {
140 | background-color: color-mix(in srgb, $black 85%, $white);
141 | }
142 | }
143 | }
144 | }
145 |
146 | #tints-and-shades {
147 | .palette-titlebar {
148 |
149 | &-name {
150 | color: $gray-100;
151 | }
152 |
153 | &-action {
154 | color: $gray-100;
155 | }
156 | }
157 |
158 | .palette-complement-dropdown {
159 | &-menu {
160 | background-color: $black;
161 | }
162 |
163 | &-item {
164 | color: $gray-100;
165 |
166 | &:is(:hover, :focus-visible) {
167 | background-color: $gray-600;
168 | color: $white;
169 | }
170 |
171 | &:active {
172 | background-color: color-mix(in srgb, $gray-600 92%, $white);
173 | }
174 | }
175 | }
176 | }
177 | }
178 |
179 | .utility-dialog {
180 | background-color: $gray-600;
181 | color: $gray-100;
182 | border-color: $gray-600;
183 |
184 | &-close {
185 | color: $gray-100;
186 | }
187 |
188 | &.export-dialog {
189 | .export-tab {
190 | color: $gray-300;
191 |
192 | &:hover {
193 | color: $gray-100;
194 | }
195 |
196 | &.is-active {
197 | border-color: transparent;
198 | color: $gray-100;
199 | background-color: $black-transparent;
200 | }
201 | }
202 |
203 | .export-body {
204 | .export-output {
205 | border-color: transparent;
206 | background-color: $black-transparent;
207 |
208 | &-code {
209 | color: $gray-100;
210 | }
211 | }
212 | }
213 | }
214 |
215 | &.share-dialog {
216 | .share-input {
217 | background-color: $black-transparent;
218 | color: $gray-100;
219 | }
220 | }
221 | }
222 |
223 | .docs {
224 | code {
225 | background-color: $gray-500;
226 | }
227 | }
228 |
229 | .not-found {
230 | background-color: $black-transparent;
231 | color: $gray-100;
232 | }
233 | }
234 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [ ](https://maketintsandshades.com) [Tint & Shade Generator](https://maketintsandshades.com)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## What is the Tint & Shade Generator?
20 |
21 | The Tint & Shade Generator is a precision color tool for producing accurate tints (pure white added) and shades (pure black added) from a given hex color in 5%, 10%, or 20% increments.
22 |
23 | ## Why is this tool unique?
24 |
25 | It takes the math seriously. In my experience similar tools get the calculations incorrect due to rounding errors, creator preferences, or other inconsistencies.
26 |
27 | Testing shows that the output matches Chrome DevTools’ calculation method as well as some [established](https://css-tricks.com/snippets/sass/tint-shade-functions), [popular](https://sindresorhus.com/sass-extras/#color-function-tint) methods to derive tints and shades via Sass.
28 |
29 | ## When would I use this?
30 |
31 | Originally created for designer and developer teams, it’s also useful for teachers, data pros, brand strategists, presentation makers, and anyone who works with colors.
32 |
33 | It’s perfect for:
34 |
35 | - exploring and refining colors visually
36 | - moving from a single color to a complete system
37 | - generating consistent tints and shades for UI states
38 | - building complementary palettes for accents or secondary UI
39 | - sharing palettes via link or image
40 | - exporting colors for design tokens, CSS, or JSON
41 |
42 | ## Calculation method
43 |
44 | The given hex color is first converted to RGB. Each RGB component is then calculated independently as follows:
45 |
46 | - **Tints:** `New value = current value + ((255 - current value) x tint factor)`
47 | - **Shades:** `New value = current value x shade factor`
48 |
49 | The new value is rounded to the nearest whole number (values ending in .5 round up), then converted back to hex for display.
50 |
51 | ## Example calculation
52 |
53 | Let’s say we want tints and shades of [Rebecca Purple](https://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/), #663399.
54 |
55 | ### 10% tint
56 |
57 | 1. #663399 is converted to the RGB equivalent of 102, 51, 153
58 | 2. **R:** `102 + ((255 - 102) x .1) = 117.3`, rounded to 117
59 | 3. **G:** `51 + ((255 - 51) x .1) = 71.4`, rounded to 71
60 | 4. **B:** `153 + ((255 - 153) x .1) = 163.2`, rounded to 163
61 | 5. RGB 117, 71, 163 is converted to the hex equivalent of #7547a3
62 |
63 | ### 10% shade
64 |
65 | 1. #663399 is converted to the RGB equivalent of 102, 51, 153
66 | 2. **R:** `102 x .9 = 91.8`, rounded to 92
67 | 3. **G:** `51 x .9 = 45.9`, rounded to 46
68 | 4. **B:** `153 x .9 = 137.7`, rounded to 138
69 | 5. RGB 92, 46, 138 is converted to the hex equivalent of #5c2e8a
70 |
71 | ## Related colors
72 |
73 | In addition to generating tints and shades, you can also add related palettes based on common color-wheel relationships. These palettes shift the hue while preserving the original saturation and lightness, which is well suited for most color systems.
74 |
75 | ### Complementary
76 |
77 | Adds one new palette using the hue directly opposite the base color (180°). This produces the strongest contrast and is best when clear visual separation is needed.
78 |
79 | ### Split complementary
80 |
81 | Adds two palettes using hues 30° on either side of the complementary color. This keeps contrast high but less extreme than a direct complementary pairing.
82 |
83 | ### Analogous
84 |
85 | Adds two palettes using hues 30° on either side of the base color. These combinations are low-contrast and cohesive, making them appropriate for subtle variation.
86 |
87 | ### Triadic
88 |
89 | Adds two palettes evenly spaced at 120° intervals around the color wheel. This produces clearly distinct and energetic color relationships.
90 |
91 | ## Figma plugin
92 |
93 | Now you can generate the same meticulously-crafted tints and shades without leaving your canvas (and automatically create local color styles, too). Grab the plugin [from the Figma Community](https://www.figma.com/community/plugin/1580658889126377365/tint-shade-generator).
94 |
95 | ## Feedback and contributing
96 |
97 | This project is open source and I’d love your help!
98 |
99 | If you notice a bug or want a feature added please [file an issue on GitHub](https://github.com/edelstone/tints-and-shades/issues/new). If you don’t have an account there, just [email me](mailto:contact@maketintsandshades.com) the details.
100 |
101 | If you’re a developer and want to help with the project, please comment on [open issues](https://github.com/edelstone/tints-and-shades/issues) or create a new one and communicate your intentions. Once we agree on a path forward you can just make a pull request and take it to the finish line.
102 |
103 | ## Local development
104 |
105 | _Prerequisites: Node.js 18+_
106 |
107 | 1. Clone this project.
108 | 2. Navigate to the project in your terminal.
109 | 3. Install dependencies: `npm install`.
110 | 4. Start the server: `npm run start`.
111 | 5. Navigate to `localhost:8080` in your browser.
112 |
113 | ## Support this project
114 |
115 | The Tint & Shade Generator will always be free but your support is greatly appreciated.
116 |
117 | - [Buy Me a Coffee](https://www.buymeacoffee.com/edelstone)
118 | - [Cash App](https://cash.app/$edelstone)
119 | - [Paypal](https://www.paypal.me/edelstone)
120 | - [Venmo](https://venmo.com/michaeledelstone)
121 |
122 | ## Credits
123 |
124 | [Michael Edelstone](https://michaeledelstone.com) designed and organized the project with major assistance from [Nick Wing](https://github.com/wickning1) on the color calculations.
125 |
126 | We use these amazing open-source libraries across the project:
127 |
128 | - [AnchorJS](https://github.com/bryanbraun/anchorjs)
129 | - [clipboard.js](https://github.com/zenorocha/clipboard.js)
130 | - [Color Names](https://github.com/meodai/color-names)
131 | - [Eleventy](https://github.com/11ty/eleventy)
132 | - [Prism](https://github.com/PrismJS/prism)
133 |
134 | Many thanks to [Joel Carr](https://github.com/joelcarr), [Sebastian Gutierrez](https://github.com/pepas24), [Tim Scalzo](https://github.com/TJScalzo), [Aman Agarwal](https://github.com/AmanAgarwal041), [Aleksandr Hovhannisyan](https://github.com/AleksandrHovhannisyan), [Shubhendu Sen](https://github.com/Sen-442b), and [Luis Escarrilla](https://github.com/latesc) for their valuable contributions.
135 |
136 | ## Design specs
137 |
138 | - Typography: [Work Sans](https://weiweihuanghuang.github.io/Work-Sans/) by Wei Huang
139 | - Iconography: [Tabler Icons](https://tabler.io/icons)
140 | - Colors: [#000000](/#colors=000000), [#ffffff](/#colors=ffffff), [#e96443](/#colors=e96443), and [#ca228e](/#colors=ca228e)
141 |
142 | Prefer Google’s color logic? Try the [Material Design Palette Generator](https://materialpalettes.com).
143 |
--------------------------------------------------------------------------------
/src/about.njk:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base
3 | title: About
4 | permalink: /about/
5 | docs: true
6 | ---
7 |
8 |
9 | What is the Tint & Shade Generator?
10 |
11 | The Tint & Shade Generator is a precision color tool for producing accurate tints (pure white added) and shades (pure black added) from a given hex color in 5%, 10%, or 20% increments.
12 |
13 | Why is this tool unique?
14 |
15 | It takes the math seriously. In my experience similar tools get the calculations incorrect due to rounding errors, creator preferences, or other inconsistencies.
16 |
17 | Testing shows that the output matches Chrome DevTools’ calculation method as well as some established , popular methods to derive tints and shades via Sass.
18 |
19 | When would I use this?
20 |
21 | Originally created for designer and developer teams, it’s also useful for teachers, data pros, brand strategists, presentation makers, and anyone who works with colors.
22 |
23 | It’s perfect for:
24 |
25 |
26 | exploring and refining colors visually
27 | moving from a single color to a complete system
28 | generating consistent tints and shades for UI states
29 | building complementary palettes for accents or secondary UI
30 | sharing palettes via link or image
31 | exporting colors for design tokens, CSS, or JSON
32 |
33 |
34 | Calculation method
35 |
36 | The given hex color is first converted to RGB. Each RGB component is then calculated independently as follows:
37 |
38 |
39 | Tints: New value = current value + ((255 - current value) x tint factor)
40 | Shades: New value = current value x shade factor
41 |
42 |
43 | The new value is rounded to the nearest whole number (values ending in .5 round up), then converted back to hex for display.
44 |
45 | Example calculation
46 |
47 | Let’s say we want tints and shades of Rebecca Purple , #663399.
48 |
49 | 10% tint
50 |
51 |
52 | #663399 is converted to the RGB equivalent of 102, 51, 153
53 | R: 102 + ((255 - 102) x .1) = 117.3, rounded to 117
54 | G: 51 + ((255 - 51) x .1) = 71.4, rounded to 71
55 | B: 153 + ((255 - 153) x .1) = 163.2, rounded to 163
56 | RGB 117, 71, 163 is converted to the hex equivalent of #7547a3
57 |
58 |
59 | 10% shade
60 |
61 |
62 | #663399 is converted to the RGB equivalent of 102, 51, 153
63 | R: 102 x .9 = 91.8, rounded to 92
64 | G: 51 x .9 = 45.9, rounded to 46
65 | B: 153 x .9 = 137.7, rounded to 138
66 | RGB 92, 46, 138 is converted to the hex equivalent of #5c2e8a
67 |
68 |
69 | Related colors
70 |
71 | In addition to generating tints and shades, you can also add related palettes based on common color-wheel relationships. These palettes shift the hue while preserving the original saturation and lightness, which is well suited for most color systems.
72 |
73 | Complementary
74 |
75 | Adds one new palette using the hue directly opposite the base color (180°). This produces the strongest contrast and is best when clear visual separation is needed.
76 |
77 | Split complementary
78 |
79 | Adds two palettes using hues 30° on either side of the complementary color. This keeps contrast high but less extreme than a direct complementary pairing.
80 |
81 | Analogous
82 |
83 | Adds two palettes using hues 30° on either side of the base color. These combinations are low-contrast and cohesive, making them appropriate for subtle variation.
84 |
85 | Triadic
86 |
87 | Adds two palettes evenly spaced at 120° intervals around the color wheel. This produces clearly distinct and energetic color relationships.
88 |
89 | Figma plugin
90 |
91 | Now you can generate the same meticulously-crafted tints and shades without leaving your canvas (and automatically create local color styles, too). Grab the plugin from the Figma Community .
92 |
93 | Feedback and contributing
94 |
95 | This project is open source and I’d love your help!
96 |
97 | If you notice a bug or want a feature added please file an issue on GitHub . If you don’t have an account there, just email me the details.
98 |
99 | If you’re a developer and want to help with the project, please comment on open issues or create a new one and communicate your intentions. Once we agree on a path forward you can just make a pull request and take it to the finish line.
100 |
101 |
102 |
103 | Support this project
104 |
105 | The Tint & Shade Generator will always be free but your support is greatly appreciated.
106 |
107 |
113 |
114 | Credits
115 |
116 | Michael Edelstone designed and organized the project with major assistance from Nick Wing on the color calculations.
117 |
118 | We use these amazing open-source libraries across the project:
119 |
120 |
127 |
128 | Many thanks to Joel Carr , Sebastian Gutierrez , Tim Scalzo , Aman Agarwal , Aleksandr Hovhannisyan , Shubhendu Sen , and Luis Escarrilla for their valuable contributions.
129 |
130 | Design specs
131 |
132 |
137 |
138 | Prefer Google’s color logic? Try the Material Design Palette Generator .
139 |
140 |
--------------------------------------------------------------------------------
/src/vendor/css/coloris.min.css:
--------------------------------------------------------------------------------
1 | .clr-picker{display:none;flex-wrap:wrap;position:absolute;width:200px;z-index:1000;border-radius:10px;background-color:#fff;justify-content:flex-end;direction:ltr;box-shadow:0 0 5px rgba(0,0,0,.05),0 5px 20px rgba(0,0,0,.1);-moz-user-select:none;-webkit-user-select:none;user-select:none}.clr-picker.clr-open,.clr-picker[data-inline=true]{display:flex}.clr-picker[data-inline=true]{position:relative}.clr-gradient{position:relative;width:100%;height:100px;margin-bottom:15px;border-radius:3px 3px 0 0;background-image:linear-gradient(rgba(0,0,0,0),#000),linear-gradient(90deg,#fff,currentColor);cursor:pointer}.clr-marker{position:absolute;width:12px;height:12px;margin:-6px 0 0 -6px;border:1px solid #fff;border-radius:50%;background-color:currentColor;cursor:pointer}.clr-picker input[type=range]::-webkit-slider-runnable-track{width:100%;height:16px}.clr-picker input[type=range]::-webkit-slider-thumb{width:16px;height:16px;-webkit-appearance:none}.clr-picker input[type=range]::-moz-range-track{width:100%;height:16px;border:0}.clr-picker input[type=range]::-moz-range-thumb{width:16px;height:16px;border:0}.clr-hue{background-image:linear-gradient(to right,red 0,#ff0 16.66%,#0f0 33.33%,#0ff 50%,#00f 66.66%,#f0f 83.33%,red 100%)}.clr-alpha,.clr-hue{position:relative;width:calc(100% - 40px);height:8px;margin:5px 20px;border-radius:4px}.clr-alpha span{display:block;height:100%;width:100%;border-radius:inherit;background-image:linear-gradient(90deg,rgba(0,0,0,0),currentColor)}.clr-alpha input[type=range],.clr-hue input[type=range]{position:absolute;width:calc(100% + 32px);height:16px;left:-16px;top:-4px;margin:0;background-color:transparent;opacity:0;cursor:pointer;appearance:none;-webkit-appearance:none}.clr-alpha div,.clr-hue div{position:absolute;width:16px;height:16px;left:0;top:50%;margin-left:-8px;transform:translateY(-50%);border:2px solid #fff;border-radius:50%;background-color:currentColor;box-shadow:0 0 1px #888;pointer-events:none}.clr-alpha div:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border-radius:50%;background-color:currentColor}.clr-format{display:none;order:1;width:calc(100% - 40px);margin:0 20px 20px}.clr-segmented{display:flex;position:relative;width:100%;margin:0;padding:0;border:1px solid #ddd;border-radius:15px;box-sizing:border-box;color:#999;font-size:12px}.clr-segmented input,.clr-segmented legend{position:absolute;width:100%;height:100%;margin:0;padding:0;border:0;left:0;top:0;opacity:0;pointer-events:none}.clr-segmented label{flex-grow:1;margin:0;padding:4px 0;font-size:inherit;font-weight:400;line-height:initial;text-align:center;cursor:pointer}.clr-segmented label:first-of-type{border-radius:10px 0 0 10px}.clr-segmented label:last-of-type{border-radius:0 10px 10px 0}.clr-segmented input:checked+label{color:#fff;background-color:#666}.clr-swatches{order:2;width:calc(100% - 32px);margin:0 16px}.clr-swatches div{display:flex;flex-wrap:wrap;padding-bottom:12px;justify-content:center}.clr-swatches button{position:relative;width:20px;height:20px;margin:0 4px 6px 4px;padding:0;border:0;border-radius:50%;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;cursor:pointer}.clr-swatches button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}input.clr-color{order:1;width:calc(100% - 80px);height:32px;margin:15px 20px 20px auto;padding:0 10px;border:1px solid #ddd;border-radius:16px;color:#444;background-color:#fff;font-family:sans-serif;font-size:14px;text-align:center;box-shadow:none}input.clr-color:focus{outline:0;border:1px solid #1e90ff}.clr-clear,.clr-close{display:none;order:2;height:24px;margin:0 20px 20px;padding:0 20px;border:0;border-radius:12px;color:#fff;background-color:#666;font-family:inherit;font-size:12px;font-weight:400;cursor:pointer}.clr-close{display:block;margin:0 20px 20px auto}.clr-preview{position:relative;width:32px;height:32px;margin:15px 0 20px 20px;border-radius:50%;overflow:hidden}.clr-preview:after,.clr-preview:before{content:'';position:absolute;height:100%;width:100%;left:0;top:0;border:1px solid #fff;border-radius:50%}.clr-preview:after{border:0;background-color:currentColor;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}.clr-preview button{position:absolute;width:100%;height:100%;z-index:1;margin:0;padding:0;border:0;border-radius:50%;outline-offset:-2px;background-color:transparent;text-indent:-9999px;cursor:pointer;overflow:hidden}.clr-alpha div,.clr-color,.clr-hue div,.clr-marker{box-sizing:border-box}.clr-field{display:inline-block;position:relative;color:transparent}.clr-field input{margin:0;direction:ltr}.clr-field.clr-rtl input{text-align:right}.clr-field button{position:absolute;width:30px;height:100%;right:0;top:50%;transform:translateY(-50%);margin:0;padding:0;border:0;color:inherit;text-indent:-1000px;white-space:nowrap;overflow:hidden;pointer-events:none}.clr-field.clr-rtl button{right:auto;left:0}.clr-field button:after{content:'';display:block;position:absolute;width:100%;height:100%;left:0;top:0;border-radius:inherit;background-color:currentColor;box-shadow:inset 0 0 1px rgba(0,0,0,.5)}.clr-alpha,.clr-alpha div,.clr-field button,.clr-preview:before,.clr-swatches button{background-image:repeating-linear-gradient(45deg,#aaa 25%,transparent 25%,transparent 75%,#aaa 75%,#aaa),repeating-linear-gradient(45deg,#aaa 25%,#fff 25%,#fff 75%,#aaa 75%,#aaa);background-position:0 0,4px 4px;background-size:8px 8px}.clr-marker:focus{outline:0}.clr-keyboard-nav .clr-alpha input:focus+div,.clr-keyboard-nav .clr-hue input:focus+div,.clr-keyboard-nav .clr-marker:focus,.clr-keyboard-nav .clr-segmented input:focus+label{outline:0;box-shadow:0 0 0 2px #1e90ff,0 0 2px 2px #fff}.clr-picker[data-alpha=false] .clr-alpha{display:none}.clr-picker[data-minimal=true]{padding-top:16px}.clr-picker[data-minimal=true] .clr-alpha,.clr-picker[data-minimal=true] .clr-color,.clr-picker[data-minimal=true] .clr-gradient,.clr-picker[data-minimal=true] .clr-hue,.clr-picker[data-minimal=true] .clr-preview{display:none}.clr-dark{background-color:#444}.clr-dark .clr-segmented{border-color:#777}.clr-dark .clr-swatches button:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}.clr-dark input.clr-color{color:#fff;border-color:#777;background-color:#555}.clr-dark input.clr-color:focus{border-color:#1e90ff}.clr-dark .clr-preview:after{box-shadow:inset 0 0 0 1px rgba(255,255,255,.5)}.clr-dark .clr-alpha,.clr-dark .clr-alpha div,.clr-dark .clr-preview:before,.clr-dark .clr-swatches button{background-image:repeating-linear-gradient(45deg,#666 25%,transparent 25%,transparent 75%,#888 75%,#888),repeating-linear-gradient(45deg,#888 25%,#444 25%,#444 75%,#888 75%,#888)}.clr-picker.clr-polaroid{border-radius:6px;box-shadow:0 0 5px rgba(0,0,0,.1),0 5px 30px rgba(0,0,0,.2)}.clr-picker.clr-polaroid:before{content:'';display:block;position:absolute;width:16px;height:10px;left:20px;top:-10px;border:solid transparent;border-width:0 8px 10px 8px;border-bottom-color:currentColor;box-sizing:border-box;color:#fff;filter:drop-shadow(0 -4px 3px rgba(0,0,0,.1));pointer-events:none}.clr-picker.clr-polaroid.clr-dark:before{color:#444}.clr-picker.clr-polaroid.clr-left:before{left:auto;right:20px}.clr-picker.clr-polaroid.clr-top:before{top:auto;bottom:-10px;transform:rotateZ(180deg)}.clr-polaroid .clr-gradient{width:calc(100% - 20px);height:120px;margin:10px;border-radius:3px}.clr-polaroid .clr-alpha,.clr-polaroid .clr-hue{width:calc(100% - 30px);height:10px;margin:6px 15px;border-radius:5px}.clr-polaroid .clr-alpha div,.clr-polaroid .clr-hue div{box-shadow:0 0 5px rgba(0,0,0,.2)}.clr-polaroid .clr-format{width:calc(100% - 20px);margin:0 10px 15px}.clr-polaroid .clr-swatches{width:calc(100% - 12px);margin:0 6px}.clr-polaroid .clr-swatches div{padding-bottom:10px}.clr-polaroid .clr-swatches button{width:22px;height:22px}.clr-polaroid input.clr-color{width:calc(100% - 60px);margin:10px 10px 15px auto}.clr-polaroid .clr-clear{margin:0 10px 15px 10px}.clr-polaroid .clr-close{margin:0 10px 15px auto}.clr-polaroid .clr-preview{margin:10px 0 15px 10px}.clr-picker.clr-large{width:275px}.clr-large .clr-gradient{height:150px}.clr-large .clr-swatches button{width:22px;height:22px}.clr-picker.clr-pill{width:380px;padding-left:180px;box-sizing:border-box}.clr-pill .clr-gradient{position:absolute;width:180px;height:100%;left:0;top:0;margin-bottom:0;border-radius:3px 0 0 3px}.clr-pill .clr-hue{margin-top:20px}
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | const SETTINGS_STORAGE_KEY = "settings";
2 | const settings = { copyWithHashtag: false, tintShadeCount: 10 };
3 | const tintShadeOptions = [5, 10, 20];
4 |
5 | const setActiveCountButtons = (buttons, count) => {
6 | buttons.forEach((btn) => {
7 | const btnValue = parseInt(btn.getAttribute("data-count"), 10);
8 | const isActive = btnValue === count;
9 | btn.classList.toggle("is-active", isActive);
10 | btn.setAttribute("aria-pressed", isActive ? "true" : "false");
11 | btn.setAttribute("tabindex", isActive ? "0" : "-1");
12 | });
13 | };
14 | const TOOLTIP_IMMEDIATE_ATTR = "data-tooltip-immediate";
15 | let lastInteractionWasKeyboard = false;
16 |
17 | const wireTooltipHandlers = () => {
18 | document.addEventListener(
19 | "pointerdown",
20 | () => {
21 | lastInteractionWasKeyboard = false;
22 | },
23 | true
24 | );
25 |
26 | document.addEventListener(
27 | "keydown",
28 | (event) => {
29 | if (event.key === "Tab") {
30 | lastInteractionWasKeyboard = true;
31 | }
32 | },
33 | true
34 | );
35 |
36 | document.addEventListener(
37 | "focusout",
38 | (event) => {
39 | const target = event.target?.closest?.("[data-tooltip]");
40 | if (target && lastInteractionWasKeyboard) {
41 | target.setAttribute(TOOLTIP_IMMEDIATE_ATTR, "true");
42 | }
43 | },
44 | true
45 | );
46 |
47 | document.addEventListener(
48 | "focusin",
49 | (event) => {
50 | const target = event.target?.closest?.("[data-tooltip]");
51 | if (target) {
52 | target.removeAttribute(TOOLTIP_IMMEDIATE_ATTR);
53 | }
54 | },
55 | true
56 | );
57 | };
58 |
59 | const loadSettings = () => {
60 | try {
61 | const savedSettings = localStorage.getItem(SETTINGS_STORAGE_KEY);
62 | if (!savedSettings) return;
63 | const parsed = JSON.parse(savedSettings);
64 | if (parsed && typeof parsed === "object") {
65 | Object.assign(settings, parsed);
66 | }
67 | } catch (e) {
68 | // ignore bad or unavailable storage
69 | }
70 | };
71 |
72 | const saveSettings = () => {
73 | try {
74 | localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
75 | } catch (e) {
76 | // ignore
77 | }
78 | };
79 |
80 | const suppressTooltipUntilMouseOut = (target) => {
81 | if (!target || !target.setAttribute) return;
82 | target.setAttribute("data-tooltip-suppressed", "true");
83 | const clear = () => {
84 | target.removeAttribute("data-tooltip-suppressed");
85 | };
86 | target.addEventListener("mouseleave", clear, { once: true });
87 | target.addEventListener("pointerleave", clear, { once: true });
88 | };
89 |
90 | const updateHashtagToggle = (button, isOn) => {
91 | if (!button) return;
92 | button.setAttribute("aria-pressed", isOn ? "true" : "false");
93 | button.classList.toggle("is-active", isOn);
94 | button.setAttribute("aria-label", isOn ? "Include hashtag when copying" : "Hide hashtag when copying");
95 | button.setAttribute("data-tooltip", isOn ? "Hide #" : "Show #");
96 | };
97 |
98 | // Initialize the settings and UI state
99 | const initializeSettings = (initialUrlState = {}) => {
100 | loadSettings();
101 | if (typeof initialUrlState.copyWithHashtag === "boolean") {
102 | settings.copyWithHashtag = initialUrlState.copyWithHashtag;
103 | }
104 | if (typeof initialUrlState.tintShadeCount === "number") {
105 | settings.tintShadeCount = palettes.normalizeTintShadeCount(initialUrlState.tintShadeCount);
106 | }
107 |
108 | const colorValuesElement = document.getElementById("color-values");
109 | const hashtagToggle = document.getElementById("show-hide-hashtags");
110 | const stepSelector = document.querySelector(".inline-actions .step-selector");
111 | const tintShadeButtons = stepSelector ? Array.from(stepSelector.querySelectorAll(".step-selector-option")) : [];
112 |
113 | if (hashtagToggle) {
114 | updateHashtagToggle(hashtagToggle, settings.copyWithHashtag);
115 | hashtagToggle.addEventListener("pointerdown", () => {
116 | suppressTooltipUntilMouseOut(hashtagToggle);
117 | });
118 | hashtagToggle.addEventListener("click", () => {
119 | settings.copyWithHashtag = !settings.copyWithHashtag;
120 | updateHashtagToggle(hashtagToggle, settings.copyWithHashtag);
121 | saveSettings();
122 | exportUI.updateClipboardData(settings.copyWithHashtag);
123 | exportUI.updateExportOutput(exportUI.state, exportUI.elements);
124 | if (palettes.updateHexValueDisplay) {
125 | palettes.updateHexValueDisplay(settings.copyWithHashtag);
126 | }
127 | if (palettes.updateHashState && palettes.parseColorValues && colorValuesElement) {
128 | const parsedColors = palettes.parseColorValues(colorValuesElement.value) || [];
129 | if (!parsedColors.length) return;
130 | palettes.updateHashState(parsedColors, settings);
131 | }
132 | });
133 | }
134 |
135 | if (tintShadeButtons.length) {
136 | if (!tintShadeOptions.includes(settings.tintShadeCount)) {
137 | settings.tintShadeCount = 10;
138 | }
139 | setActiveCountButtons(tintShadeButtons, settings.tintShadeCount);
140 |
141 | const activateIndex = (nextIndex) => {
142 | const target = tintShadeButtons[nextIndex];
143 | if (!target) return;
144 | const nextValue = parseInt(target.getAttribute("data-count"), 10);
145 | if (!tintShadeOptions.includes(nextValue)) return;
146 | if (settings.tintShadeCount === nextValue) {
147 | target.focus();
148 | return;
149 | }
150 | settings.tintShadeCount = nextValue;
151 | setActiveCountButtons(tintShadeButtons, settings.tintShadeCount);
152 | saveSettings();
153 | palettes.createTintsAndShades(settings, false, { skipScroll: true, skipFocus: true });
154 | target.focus();
155 | };
156 |
157 | tintShadeButtons.forEach((button, index) => {
158 | button.addEventListener("click", () => {
159 | activateIndex(index);
160 | });
161 |
162 | button.addEventListener("keydown", (event) => {
163 | if (event.key === "ArrowRight" || event.key === "ArrowDown") {
164 | event.preventDefault();
165 | const nextIndex = (index + 1) % tintShadeButtons.length;
166 | activateIndex(nextIndex);
167 | } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
168 | event.preventDefault();
169 | const prevIndex = (index - 1 + tintShadeButtons.length) % tintShadeButtons.length;
170 | activateIndex(prevIndex);
171 | } else if (event.key === "Home") {
172 | event.preventDefault();
173 | activateIndex(0);
174 | } else if (event.key === "End") {
175 | event.preventDefault();
176 | activateIndex(tintShadeButtons.length - 1);
177 | } else if (event.key === "Enter" || event.key === " ") {
178 | event.preventDefault();
179 | activateIndex(index);
180 | }
181 | });
182 | });
183 | }
184 | };
185 |
186 | document.addEventListener("DOMContentLoaded", () => {
187 | wireTooltipHandlers();
188 | const urlState = palettes.readHashState ? palettes.readHashState() : {};
189 | initializeSettings(urlState);
190 | exportUI.wireExportControls();
191 |
192 | const colorValuesElement = document.getElementById("color-values");
193 | if (colorValuesElement) {
194 | colorValuesElement.value = urlState.colors || "";
195 | } else {
196 | console.error("Element with id 'color-values' not found.");
197 | }
198 |
199 | palettes.createTintsAndShades(settings, true);
200 |
201 | const colorEntryForm = document.getElementById("color-entry-form");
202 | if (colorEntryForm) {
203 | colorEntryForm.addEventListener("submit", (e) => {
204 | e.preventDefault();
205 | const {
206 | skipScroll = false,
207 | skipFocus = false,
208 | focusPickerContext = null
209 | } = e.detail || {};
210 | palettes.createTintsAndShades(settings, false, { skipScroll, skipFocus, focusPickerContext });
211 | });
212 | } else {
213 | console.error("Element with id 'color-entry-form' not found.");
214 | }
215 |
216 | const copyWithHashtagToggle = document.getElementById("show-hide-hashtags");
217 | if (!copyWithHashtagToggle) {
218 | console.error("Element with id 'show-hide-hashtags' not found.");
219 | }
220 | });
221 |
222 | document.addEventListener("click", (event) => {
223 | if (event.target.id === "make") {
224 | if (!document.getElementById("carbonads")) return;
225 | if (typeof _carbonads !== "undefined") _carbonads.refresh();
226 | }
227 | });
228 |
--------------------------------------------------------------------------------
/src/vendor/js/clipboard.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * clipboard.js v2.0.11
3 | * https://clipboardjs.com/
4 | *
5 | * Licensed MIT © Zeno Rocha
6 | */
7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1 {
2 | const shareElements = {
3 | openButton: document.getElementById("share-open"),
4 | modal: document.getElementById("share-dialog"),
5 | closeButton: document.getElementById("share-close"),
6 | input: document.getElementById("share-link-input"),
7 | copyButton: document.getElementById("share-copy"),
8 | copyStatus: document.getElementById("share-copy-status")
9 | };
10 |
11 | let pageScrollY = 0;
12 | let handleOutsidePointerDown = null;
13 |
14 | const prefersReducedMotion = () => {
15 | return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
16 | };
17 |
18 | const lockBodyScroll = () => {
19 | if (document.body.classList.contains("modal-open")) return;
20 | pageScrollY = window.scrollY || document.documentElement.scrollTop || 0;
21 | document.body.style.position = "fixed";
22 | document.body.style.top = `-${pageScrollY}px`;
23 | document.body.style.left = "0";
24 | document.body.style.right = "0";
25 | document.body.style.width = "100%";
26 | document.body.classList.add("modal-open");
27 | };
28 |
29 | const unlockBodyScroll = () => {
30 | const scrollToY = pageScrollY || 0;
31 | document.body.classList.remove("modal-open");
32 | document.body.style.position = "";
33 | document.body.style.top = "";
34 | document.body.style.left = "";
35 | document.body.style.right = "";
36 | document.body.style.width = "";
37 | window.scrollTo(0, scrollToY);
38 | };
39 |
40 | const getDialogFocusable = () => {
41 | if (!shareElements.modal) return [];
42 | const selectors = [
43 | "button",
44 | "[href]",
45 | 'input:not([type="hidden"])',
46 | "select",
47 | "textarea",
48 | "[tabindex]:not([tabindex='-1'])"
49 | ];
50 | const nodes = Array.from(shareElements.modal.querySelectorAll(selectors.join(",")));
51 | return nodes.filter((node) => {
52 | const tabIndex = node.tabIndex;
53 | const isHidden = node.getAttribute("aria-hidden") === "true";
54 | const isDisabled = node.hasAttribute("disabled");
55 | return !isHidden && !isDisabled && tabIndex !== -1;
56 | });
57 | };
58 |
59 | const resizeShareInputHeight = () => {
60 | if (!shareElements.input) return;
61 | shareElements.input.style.height = "auto";
62 | shareElements.input.style.height = `${shareElements.input.scrollHeight}px`;
63 | };
64 |
65 | const updateShareLinkValue = () => {
66 | if (!shareElements.input) return;
67 | shareElements.input.value = window.location.href;
68 | resizeShareInputHeight();
69 | };
70 |
71 | const copyShareLink = async () => {
72 | if (!shareElements.input) return;
73 | const text = shareElements.input.value;
74 | if (!text) return;
75 | try {
76 | if (navigator.clipboard && navigator.clipboard.writeText) {
77 | await navigator.clipboard.writeText(text);
78 | } else {
79 | const helper = document.createElement("textarea");
80 | helper.value = text;
81 | helper.setAttribute("readonly", "");
82 | helper.style.position = "absolute";
83 | helper.style.left = "-9999px";
84 | document.body.appendChild(helper);
85 | helper.select();
86 | document.execCommand("copy");
87 | document.body.removeChild(helper);
88 | }
89 | const btn = shareElements.copyButton;
90 | if (btn) {
91 | btn.classList.add("copied");
92 | btn.disabled = true;
93 | btn.setAttribute("aria-disabled", "true");
94 | setTimeout(() => {
95 | btn.classList.remove("copied");
96 | btn.disabled = false;
97 | btn.setAttribute("aria-disabled", "false");
98 | }, 1500);
99 | }
100 | const status = shareElements.copyStatus;
101 | if (status) {
102 | status.textContent = "Copied share link to clipboard.";
103 | setTimeout(() => {
104 | status.textContent = "";
105 | }, 4500);
106 | }
107 | } catch (err) {
108 | console.error(err);
109 | }
110 | };
111 |
112 | const focusCopyButton = () => {
113 | if (!shareElements.copyButton) return;
114 | try {
115 | shareElements.copyButton.focus({ preventScroll: true });
116 | } catch (error) {
117 | shareElements.copyButton.focus();
118 | }
119 | };
120 |
121 | const openShareModal = () => {
122 | if (!shareElements.modal) return;
123 | updateShareLinkValue();
124 | lockBodyScroll();
125 | if (typeof shareElements.modal.showModal === "function") {
126 | shareElements.modal.showModal();
127 | } else {
128 | shareElements.modal.setAttribute("open", "true");
129 | }
130 | shareElements.modal.classList.remove("is-closing");
131 | shareElements.modal.removeAttribute("data-closing");
132 | if (!prefersReducedMotion()) {
133 | shareElements.modal.classList.add("is-opening");
134 | shareElements.modal.addEventListener("animationend", (event) => {
135 | if (event.target === shareElements.modal && event.animationName === "export-dialog-fade") {
136 | shareElements.modal.classList.remove("is-opening");
137 | }
138 | }, { once: true });
139 | } else {
140 | shareElements.modal.classList.remove("is-opening");
141 | }
142 | if (!handleOutsidePointerDown) {
143 | handleOutsidePointerDown = (event) => {
144 | if (!shareElements.modal || !shareElements.modal.open) return;
145 | const rect = shareElements.modal.getBoundingClientRect();
146 | const isOutside = event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
147 | if (isOutside) {
148 | closeShareModal();
149 | }
150 | };
151 | document.addEventListener("pointerdown", handleOutsidePointerDown);
152 | }
153 | if (shareElements.openButton) {
154 | shareElements.openButton.setAttribute("aria-expanded", "true");
155 | }
156 | requestAnimationFrame(() => {
157 | resizeShareInputHeight();
158 | focusCopyButton();
159 | });
160 | };
161 |
162 | const closeShareModal = () => {
163 | const modal = shareElements.modal;
164 | if (!modal) return;
165 | if (modal.getAttribute("data-closing") === "true") return;
166 |
167 | const completeClose = () => {
168 | modal.removeAttribute("data-closing");
169 | modal.classList.remove("is-closing");
170 | modal.classList.remove("is-opening");
171 | if (modal.open && typeof modal.close === "function") {
172 | modal.close();
173 | } else {
174 | modal.removeAttribute("open");
175 | }
176 | unlockBodyScroll();
177 | if (handleOutsidePointerDown) {
178 | document.removeEventListener("pointerdown", handleOutsidePointerDown);
179 | handleOutsidePointerDown = null;
180 | }
181 | if (shareElements.openButton) {
182 | shareElements.openButton.setAttribute("aria-expanded", "false");
183 | }
184 | };
185 |
186 | if (!modal.open && !modal.hasAttribute("open")) {
187 | completeClose();
188 | return;
189 | }
190 |
191 | if (prefersReducedMotion()) {
192 | completeClose();
193 | return;
194 | }
195 |
196 | modal.setAttribute("data-closing", "true");
197 | modal.classList.remove("is-opening");
198 | modal.classList.add("is-closing");
199 |
200 | let closeFallbackTimer = null;
201 | const handleAnimationEnd = (event) => {
202 | if (event.target !== modal || event.animationName !== "export-dialog-fade") return;
203 | clearTimeout(closeFallbackTimer);
204 | modal.removeEventListener("animationend", handleAnimationEnd);
205 | completeClose();
206 | };
207 |
208 | closeFallbackTimer = setTimeout(() => {
209 | modal.removeEventListener("animationend", handleAnimationEnd);
210 | completeClose();
211 | }, 250);
212 |
213 | modal.addEventListener("animationend", handleAnimationEnd);
214 | };
215 |
216 | const wireShareControls = () => {
217 | if (shareElements.openButton) {
218 | shareElements.openButton.addEventListener("click", () => openShareModal());
219 | }
220 |
221 | if (shareElements.closeButton) {
222 | shareElements.closeButton.addEventListener("click", () => closeShareModal());
223 | }
224 |
225 | if (shareElements.modal) {
226 | shareElements.modal.addEventListener("cancel", (event) => {
227 | event.preventDefault();
228 | closeShareModal();
229 | });
230 | shareElements.modal.addEventListener("click", (event) => {
231 | event.stopPropagation();
232 | });
233 | shareElements.modal.addEventListener("keydown", (event) => {
234 | if (event.key === "Escape") {
235 | event.preventDefault();
236 | closeShareModal();
237 | return;
238 | }
239 | if (event.key === "Tab") {
240 | const focusables = getDialogFocusable();
241 | if (!focusables.length) return;
242 | const currentIndex = focusables.indexOf(document.activeElement);
243 | let nextIndex = currentIndex;
244 | if (event.shiftKey) {
245 | nextIndex = currentIndex <= 0 ? focusables.length - 1 : currentIndex - 1;
246 | } else {
247 | nextIndex = currentIndex === focusables.length - 1 ? 0 : currentIndex + 1;
248 | }
249 | focusables[nextIndex].focus();
250 | event.preventDefault();
251 | }
252 | });
253 | }
254 |
255 | if (shareElements.copyButton) {
256 | shareElements.copyButton.addEventListener("click", () => copyShareLink());
257 | }
258 |
259 | if (shareElements.input) {
260 | shareElements.input.setAttribute("tabindex", "-1");
261 | shareElements.input.addEventListener("click", () => {
262 | shareElements.input.select();
263 | });
264 | }
265 | };
266 |
267 | wireShareControls();
268 |
269 | window.shareUI = {
270 | openShareModal,
271 | closeShareModal,
272 | elements: shareElements
273 | };
274 | })();
275 |
--------------------------------------------------------------------------------
/src/vendor/js/coloris.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&module.exports?module.exports=t():(e.Coloris=t(),"object"==typeof window&&e.Coloris.init())}("undefined"!=typeof self?self:void 0,function(){{var L=window,x=document,A=Math,C=void 0;const Z=x.createElement("canvas").getContext("2d"),_={r:0,g:0,b:0,h:0,s:0,v:0,a:1};let u,d,p,i,s,h,f,b,y,a,v,m,g,w,l,n,k={};const ee={el:"[data-coloris]",parent:"body",theme:"default",themeMode:"light",rtl:!1,wrap:!0,margin:2,format:"hex",formatToggle:!1,swatches:[],swatchesOnly:!1,alpha:!0,forceAlpha:!1,focusInput:!0,selectInput:!1,inline:!1,defaultColor:"#000000",clearButton:!1,clearLabel:"Clear",closeButton:!1,closeLabel:"Close",onChange:()=>C,a11y:{open:"Open color picker",close:"Close color picker",clear:"Clear the selected color",marker:"Saturation: {s}. Brightness: {v}.",hueSlider:"Hue slider",alphaSlider:"Opacity slider",input:"Color value field",format:"Color format",swatch:"Color swatch",instruction:"Saturation and brightness selector. Use up, down, left and right arrow keys to select."}},te={};let o="",c={},E=!1;function S(t){if("object"==typeof t)for(const o in t)switch(o){case"el":B(t.el),!1!==t.wrap&&H(t.el);break;case"parent":(u=t.parent instanceof HTMLElement?t.parent:x.querySelector(t.parent))&&(u.appendChild(d),ee.parent=t.parent,u===x.body)&&(u=C);break;case"themeMode":ee.themeMode=t.themeMode,"auto"===t.themeMode&&L.matchMedia&&L.matchMedia("(prefers-color-scheme: dark)").matches&&(ee.themeMode="dark");case"theme":t.theme&&(ee.theme=t.theme),d.className=`clr-picker clr-${ee.theme} clr-`+ee.themeMode,ee.inline&&M();break;case"rtl":ee.rtl=!!t.rtl,Array.from(x.getElementsByClassName("clr-field")).forEach(e=>e.classList.toggle("clr-rtl",ee.rtl));break;case"margin":t.margin*=1,ee.margin=(isNaN(t.margin)?ee:t).margin;break;case"wrap":t.el&&t.wrap&&H(t.el);break;case"formatToggle":ee.formatToggle=!!t.formatToggle,K("clr-format").style.display=ee.formatToggle?"block":"none",ee.formatToggle&&(ee.format="auto");break;case"swatches":if(Array.isArray(t.swatches)){var l=K("clr-swatches");const c=x.createElement("div");l.textContent="",t.swatches.forEach((e,t)=>{var l=x.createElement("button");l.setAttribute("type","button"),l.setAttribute("id","clr-swatch-"+t),l.setAttribute("aria-labelledby","clr-swatch-label clr-swatch-"+t),l.style.color=e,l.textContent=e,c.appendChild(l)}),t.swatches.length&&l.appendChild(c),ee.swatches=t.swatches.slice()}break;case"swatchesOnly":ee.swatchesOnly=!!t.swatchesOnly,d.setAttribute("data-minimal",ee.swatchesOnly);break;case"alpha":ee.alpha=!!t.alpha,d.setAttribute("data-alpha",ee.alpha);break;case"inline":ee.inline=!!t.inline,d.setAttribute("data-inline",ee.inline),ee.inline&&(l=t.defaultColor||ee.defaultColor,w=j(l),M(),I(l));break;case"clearButton":"object"==typeof t.clearButton&&(t.clearButton.label&&(ee.clearLabel=t.clearButton.label,f.innerHTML=ee.clearLabel),t.clearButton=t.clearButton.show),ee.clearButton=!!t.clearButton,f.style.display=ee.clearButton?"block":"none";break;case"clearLabel":ee.clearLabel=t.clearLabel,f.innerHTML=ee.clearLabel;break;case"closeButton":ee.closeButton=!!t.closeButton,ee.closeButton?d.insertBefore(b,s):s.appendChild(b);break;case"closeLabel":ee.closeLabel=t.closeLabel,b.innerHTML=ee.closeLabel;break;case"a11y":var a,r,n=t.a11y;let e=!1;if("object"==typeof n)for(const i in n)n[i]&&ee.a11y[i]&&(ee.a11y[i]=n[i],e=!0);e&&(a=K("clr-open-label"),r=K("clr-swatch-label"),a.innerHTML=ee.a11y.open,r.innerHTML=ee.a11y.swatch,b.setAttribute("aria-label",ee.a11y.close),f.setAttribute("aria-label",ee.a11y.clear),y.setAttribute("aria-label",ee.a11y.hueSlider),v.setAttribute("aria-label",ee.a11y.alphaSlider),h.setAttribute("aria-label",ee.a11y.input),p.setAttribute("aria-label",ee.a11y.instruction));break;default:ee[o]=t[o]}}function e(e,t){"string"==typeof e&&"object"==typeof t&&(te[e]=t,E=!0)}function T(e){delete te[e],0===Object.keys(te).length&&(E=!1,e===o)&&$()}function r(e){if(E){var t,l=["el","wrap","rtl","inline","defaultColor","a11y"];for(t in te){const r=te[t];if(e.matches(t)){for(var a in o=t,c={},l.forEach(e=>delete r[e]),r)c[a]=Array.isArray(ee[a])?ee[a].slice():ee[a];S(r);break}}}}function $(){0{J(e,"click",t),J(e,"input",O)}):(J(x,"click",e,t),J(x,"input",e,O))}function t(e){ee.inline||(r(e.target),g=e.target,l=g.value,w=j(l),d.classList.add("clr-open"),M(),I(l),(ee.focusInput||ee.selectInput)&&(h.focus({preventScroll:!0}),h.setSelectionRange(g.selectionStart,g.selectionEnd)),ee.selectInput&&h.select(),(n||ee.swatchesOnly)&&G().shift().focus(),g.dispatchEvent(new Event("open",{bubbles:!1})))}function M(){if(d&&(g||ee.inline)){var r=u,n=L.scrollY,o=d.offsetWidth,c=d.offsetHeight,i={left:!1,top:!1};let e,l,t,a={x:0,y:0};if(r&&(e=L.getComputedStyle(r),l=parseFloat(e.marginTop),t=parseFloat(e.borderTopWidth),(a=r.getBoundingClientRect()).y+=t+n),!ee.inline){var s=g.getBoundingClientRect();let e=s.x,t=n+s.y+s.height+ee.margin;r?(e-=a.x,t-=a.y,e+o>r.clientWidth&&(e+=s.width-o,i.left=!0),t+c>r.clientHeight-l&&c+ee.margin<=s.top-(a.y-n)&&(t-=s.height+c+2*ee.margin,i.top=!0),t+=r.scrollTop):(e+o>x.documentElement.clientWidth&&(e+=s.width-o,i.left=!0),t+c-n>x.documentElement.clientHeight&&c+ee.margin<=s.top&&(t=n+s.y-c-ee.margin,i.top=!0)),d.classList.toggle("clr-left",i.left),d.classList.toggle("clr-top",i.top),d.style.left=e+"px",d.style.top=t+"px",a.x+=d.offsetLeft,a.y+=d.offsetTop}k={width:p.offsetWidth,height:p.offsetHeight,x:p.offsetLeft+a.x,y:p.offsetTop+a.y}}}function H(e){e instanceof HTMLElement?N(e):(Array.isArray(e)?e:x.querySelectorAll(e)).forEach(N)}function N(t){var l=t.parentNode;if(!l.classList.contains("clr-field")){var a=x.createElement("div");let e="clr-field";(ee.rtl||t.classList.contains("clr-rtl"))&&(e+=" clr-rtl"),a.innerHTML=' ',l.insertBefore(a,t),a.className=e,a.style.color=t.value,a.appendChild(t)}}function O(e){var t=e.target.parentNode;t.classList.contains("clr-field")&&(t.style.color=e.target.value)}function D(e){if(g&&!ee.inline){const t=g;e&&(g=C,l!==t.value)&&(t.value=l,t.dispatchEvent(new Event("input",{bubbles:!0}))),setTimeout(()=>{l!==t.value&&t.dispatchEvent(new Event("change",{bubbles:!0}))}),d.classList.remove("clr-open"),E&&$(),t.dispatchEvent(new Event("close",{bubbles:!1})),ee.focusInput&&t.focus({preventScroll:!0}),g=C}}function I(e){var e=function(e){let t,l;Z.fillStyle="#000",Z.fillStyle=e,l=(t=/^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i.exec(Z.fillStyle))?{r:+t[3],g:+t[4],b:+t[5],a:+t[6]}:(t=Z.fillStyle.replace("#","").match(/.{2}/g).map(e=>parseInt(e,16)),{r:t[0],g:t[1],b:t[2],a:1});return l}(e),t=function(e){var t=e.r/255,l=e.g/255,a=e.b/255,r=A.max(t,l,a),n=A.min(t,l,a),n=r-n,o=r;let c=0,i=0;n&&(r===t&&(c=(l-a)/n),r===l&&(c=2+(a-t)/n),r===a&&(c=4+(t-l)/n),r)&&(i=n/r);return{h:(c=A.floor(60*c))<0?c+360:c,s:A.round(100*i),v:A.round(100*o),a:e.a}}(e);P(t.s,t.v),U(e,t),y.value=t.h,d.style.color=`hsl(${t.h}, 100%, 50%)`,a.style.left=t.h/360*100+"%",i.style.left=k.width*t.s/100+"px",i.style.top=k.height-k.height*t.v/100+"px",v.value=100*t.a,m.style.left=100*t.a+"%"}function j(e){e=e.substring(0,3).toLowerCase();return"rgb"===e||"hsl"===e?e:"hex"}function R(e){e=e!==C?e:h.value,g&&(g.value=e,g.dispatchEvent(new Event("input",{bubbles:!0}))),ee.onChange&&ee.onChange.call(L,e,g),x.dispatchEvent(new CustomEvent("coloris:pick",{detail:{color:e,currentEl:g}}))}function W(e,t){var l,a,r,n,o,e={h:+y.value,s:e/k.width*100,v:100-t/k.height*100,a:v.value/100},c=(c=(t=e).s/100,l=t.v/100,c*=l,a=t.h/60,r=c*(1-A.abs(a%2-1)),c+=l-=c,r+=l,a=A.floor(a)%6,n=[c,r,l,l,r,c][a],o=[r,c,c,r,l,l][a],l=[l,l,r,c,c,r][a],{r:A.round(255*n),g:A.round(255*o),b:A.round(255*l),a:t.a});P(e.s,e.v),U(c,e),R()}function P(e,t){let l=ee.a11y.marker;e=+e.toFixed(1),t=+t.toFixed(1),l=(l=l.replace("{s}",e)).replace("{v}",t),i.setAttribute("aria-label",l)}function q(e){var t={pageX:((t=e).changedTouches?t.changedTouches[0]:t).pageX,pageY:(t.changedTouches?t.changedTouches[0]:t).pageY},l=t.pageX-k.x;let a=t.pageY-k.y;u&&(a+=u.scrollTop),F(l,a),e.preventDefault(),e.stopPropagation()}function F(e,t){e=e<0?0:e>k.width?k.width:e,t=t<0?0:t>k.height?k.height:t,i.style.left=e+"px",i.style.top=t+"px",W(e,t),i.focus()}function U(e,t){void 0===e&&(e={}),void 0===t&&(t={});let l=ee.format;for(const o in e)_[o]=e[o];for(const c in t)_[c]=t[c];var a,r=function(e){let t=e.r.toString(16),l=e.g.toString(16),a=e.b.toString(16),r="";e.r<16&&(t="0"+t);e.g<16&&(l="0"+l);e.b<16&&(a="0"+a);ee.alpha&&(e.a<1||ee.forceAlpha)&&(e=255*e.a|0,r=e.toString(16),e<16)&&(r="0"+r);return"#"+t+l+a+r}(_),n=r.substring(0,7);switch(i.style.color=n,m.parentNode.style.color=n,m.style.color=r,s.style.color=r,p.style.display="none",p.offsetHeight,p.style.display="",m.nextElementSibling.style.display="none",m.nextElementSibling.offsetHeight,m.nextElementSibling.style.display="","mixed"===l?l=1===_.a?"hex":"rgb":"auto"===l&&(l=w),l){case"hex":h.value=r;break;case"rgb":h.value=(a=_,!ee.alpha||1===a.a&&!ee.forceAlpha?`rgb(${a.r}, ${a.g}, ${a.b})`:`rgba(${a.r}, ${a.g}, ${a.b}, ${a.a})`);break;case"hsl":h.value=(a=function(e){var t=e.v/100,l=t*(1-e.s/100/2);let a;0`+`
'+`${ee.a11y.format} `+'Hex RGB HSL
'+`${ee.clearLabel} `+''+`${ee.closeLabel} `+"
"+`${ee.a11y.open} `+`${ee.a11y.swatch} `,x.body.appendChild(d),p=K("clr-color-area"),i=K("clr-color-marker"),f=K("clr-clear"),b=K("clr-close"),s=K("clr-color-preview"),h=K("clr-color-value"),y=K("clr-hue-slider"),a=K("clr-hue-marker"),v=K("clr-alpha-slider"),m=K("clr-alpha-marker"),B(ee.el),H(ee.el),J(d,"mousedown",e=>{d.classList.remove("clr-keyboard-nav"),e.stopPropagation()}),J(p,"mousedown",e=>{J(x,"mousemove",q)}),J(p,"contextmenu",e=>{e.preventDefault()}),J(p,"touchstart",e=>{x.addEventListener("touchmove",q,{passive:!1})}),J(i,"mousedown",e=>{J(x,"mousemove",q)}),J(i,"touchstart",e=>{x.addEventListener("touchmove",q,{passive:!1})}),J(h,"change",e=>{var t=h.value;(g||ee.inline)&&R(""===t?t:I(t))}),J(f,"click",e=>{R(""),D()}),J(b,"click",e=>{R(),D()}),J(K("clr-format"),"click",".clr-format input",e=>{w=e.target.value,U(),R()}),J(d,"click",".clr-swatches button",e=>{I(e.target.textContent),R(),ee.swatchesOnly&&D()}),J(x,"mouseup",e=>{x.removeEventListener("mousemove",q)}),J(x,"touchend",e=>{x.removeEventListener("touchmove",q)}),J(x,"mousedown",e=>{n=!1,d.classList.remove("clr-keyboard-nav"),D()}),J(x,"keydown",e=>{var t,l=e.key,a=e.target,r=e.shiftKey;"Escape"===l?D(!0):"Enter"===l&&"BUTTON"!==a.tagName?D():(["Tab","ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(l)&&(n=!0,d.classList.add("clr-keyboard-nav")),"Tab"===l&&a.matches(".clr-picker *")&&(t=(l=G()).shift(),l=l.pop(),r&&a===t?(l.focus(),e.preventDefault()):r||a!==l||(t.focus(),e.preventDefault())))}),J(x,"click",".clr-field button",e=>{E&&$(),e.target.nextElementSibling.dispatchEvent(new Event("click",{bubbles:!0}))}),J(i,"keydown",e=>{var t,l={ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]};Object.keys(l).includes(e.key)&&([l,t]=[...l[e.key]],F(+i.style.left.replace("px","")+l,+i.style.top.replace("px","")+t),e.preventDefault())}),J(p,"click",q),J(y,"input",Y),J(v,"input",X))}function G(){return Array.from(d.querySelectorAll("input, button")).filter(e=>!!e.offsetWidth)}function K(e){return x.getElementById(e)}function J(e,t,l,a){const r=Element.prototype.matches||Element.prototype.msMatchesSelector;"string"==typeof l?e.addEventListener(t,e=>{r.call(e.target,l)&&a.call(e.target,e)}):(a=l,e.addEventListener(t,a))}function Q(e,t){t=t!==C?t:[],"loading"!==x.readyState?e(...t):x.addEventListener("DOMContentLoaded",()=>{e(...t)})}function le(e,t){g=t,l=g.value,r(t),w=j(e),M(),I(e),R(),l!==e&&g.dispatchEvent(new Event("change",{bubbles:!0}))}NodeList!==C&&NodeList.prototype&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=Array.prototype.forEach);var V=(()=>{const a={init:z,set:S,wrap:H,close:D,setInstance:e,setColor:le,removeInstance:T,updatePosition:M,ready:Q};function t(e){Q(()=>{e&&("string"==typeof e?B:S)(e)})}for(const r in a)t[r]=function(){for(var e=arguments.length,t=new Array(e),l=0;l{L.addEventListener("resize",e=>{t.updatePosition()}),L.addEventListener("scroll",e=>{t.updatePosition()})}),t})();return V.coloris=V}});
--------------------------------------------------------------------------------
/src/js/color-picker.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const colorInput = document.getElementById("color-values");
3 | const pickerInput = document.getElementById("color-picker-input");
4 | const pickerButton = document.getElementById("color-picker-button");
5 | const submitButton = document.getElementById("make");
6 |
7 | if (!colorInput || !pickerInput || !pickerButton || typeof Coloris === "undefined" || !window.palettes) return;
8 |
9 | const defaultColor = "#3b82f6";
10 | let pendingHex = "";
11 | let activeContext = { mode: "add", index: null };
12 | let hasCommittedThisSession = false;
13 | let shouldCommit = false;
14 | let activePickerCell = null;
15 | let isPickerOpen = false;
16 | let focusButtonOnEsc = false;
17 | let pendingFocusTarget = null;
18 | let focusReturnTarget = null;
19 | let suppressNextPalettePickerOpen = null;
20 | let suppressNextPickerButtonOpen = false;
21 | const GLOBAL_GUARD = "__tsColorPickerDocumentHandlersBound";
22 | const ACTIVATION_KEYS = new Set(["enter", "return", "numpadenter", " ", "space", "spacebar"]);
23 | const ACTIVATION_KEY_CODES = new Set([13, 32]);
24 | const isActivationKey = (value) => {
25 | if (typeof value === "string") {
26 | return ACTIVATION_KEYS.has(value.toLowerCase());
27 | }
28 | if (typeof value === "number") {
29 | return ACTIVATION_KEY_CODES.has(value);
30 | }
31 | if (value && typeof value === "object") {
32 | if (isActivationKey(value.key)) return true;
33 | if (isActivationKey(value.code)) return true;
34 | const keyCode = typeof value.keyCode === "number" ? value.keyCode : value.which;
35 | if (typeof keyCode === "number") {
36 | return isActivationKey(keyCode);
37 | }
38 | }
39 | return false;
40 | };
41 | const getEventTargetElement = (event) => {
42 | let node = event && event.target;
43 | while (node) {
44 | if (node instanceof Element) return node;
45 | node = node.parentNode;
46 | }
47 | return null;
48 | };
49 | const closestFromEvent = (event, selector) => {
50 | const target = getEventTargetElement(event);
51 | return target ? target.closest(selector) : null;
52 | };
53 |
54 | const clearActivePickerCell = () => {
55 | if (activePickerCell && activePickerCell.classList) {
56 | activePickerCell.classList.remove("is-picker-open");
57 | }
58 | activePickerCell = null;
59 | };
60 |
61 | const activatePickerCell = (target) => {
62 | clearActivePickerCell();
63 | if (target && target.classList && target.classList.contains("edit-base-button")) {
64 | activePickerCell = target;
65 | activePickerCell.classList.add("is-picker-open");
66 | }
67 | };
68 |
69 | const focusEl = (el) => {
70 | if (el && typeof el.focus === "function") {
71 | el.focus({ preventScroll: true });
72 | }
73 | };
74 |
75 | const suppressTooltipUntilMouseOut = (target) => {
76 | if (!target || !target.setAttribute) return;
77 | target.setAttribute("data-tooltip-suppressed", "true");
78 | const clear = () => {
79 | target.removeAttribute("data-tooltip-suppressed");
80 | };
81 | target.addEventListener("mouseleave", clear, { once: true });
82 | target.addEventListener("pointerleave", clear, { once: true });
83 | };
84 |
85 | const handlePickerFocusIn = (event) => {
86 | if (!isPickerOpen) return;
87 | const picker = document.getElementById("clr-picker");
88 | if (focusButtonOnEsc) return;
89 | if (picker && picker.contains(event.target)) return;
90 | const palettePickerButton = closestFromEvent(event, ".edit-base-button");
91 | if (palettePickerButton && palettePickerButton === activePickerCell) {
92 | suppressNextPalettePickerOpen = palettePickerButton;
93 | }
94 | pendingFocusTarget = event.target;
95 | Coloris.close();
96 | };
97 |
98 | const handlePickerEscape = (event) => {
99 | if (event.defaultPrevented) return;
100 | if (!isPickerOpen || event.key !== "Escape") return;
101 | event.preventDefault();
102 | event.stopImmediatePropagation();
103 | event.stopPropagation();
104 | focusButtonOnEsc = true;
105 | Coloris.close();
106 | };
107 |
108 | const getPickerCloseButton = () => document.getElementById("clr-close");
109 |
110 | const getPickerFocusableElements = () => {
111 | const picker = document.getElementById("clr-picker");
112 | if (!picker) return [];
113 | return Array.from(picker.querySelectorAll('input, button, [tabindex]:not([tabindex="-1"])')).filter((element) => {
114 | if (element.disabled) return false;
115 | if (element.getAttribute("tabindex") === "0" && element.matches("div, span, p, section")) return false;
116 | if (typeof element.tabIndex === "number" && element.tabIndex < 0) return false;
117 | if (!(element instanceof HTMLElement)) return false;
118 | return element.offsetParent !== null || element.getClientRects().length > 0;
119 | });
120 | };
121 |
122 | const getDocumentFocusableElements = () => {
123 | const focusableSelectors = [
124 | 'a[href]',
125 | 'area[href]',
126 | 'input:not([type="hidden"]):not([disabled])',
127 | 'select:not([disabled])',
128 | 'textarea:not([disabled])',
129 | 'button:not([disabled])',
130 | 'iframe',
131 | '[tabindex]:not([tabindex="-1"])',
132 | '[contenteditable="true"]'
133 | ];
134 | return Array.from(document.querySelectorAll(focusableSelectors.join(","))).filter((element) => {
135 | if (!(element instanceof HTMLElement)) return false;
136 | if (element.hasAttribute("disabled")) return false;
137 | if (element.getAttribute("tabindex") === "0" && element.matches("div, span, p, section")) return false;
138 | if (typeof element.tabIndex === "number" && element.tabIndex < 0) return false;
139 | if (element.closest && element.closest("#clr-picker")) return false;
140 | return element.offsetParent !== null || element.getClientRects().length > 0;
141 | });
142 | };
143 |
144 | const getNextFocusableAfterTrigger = (direction) => {
145 | const reference = focusReturnTarget || pickerButton;
146 | const focusableElements = getDocumentFocusableElements();
147 | if (!focusableElements.length || !reference) return null;
148 |
149 | if (reference === pickerButton && direction > 0 && submitButton) {
150 | return submitButton;
151 | }
152 |
153 | const currentIndex = focusableElements.indexOf(reference);
154 | if (currentIndex === -1) {
155 | return direction > 0 ? focusableElements[0] : focusableElements[focusableElements.length - 1];
156 | }
157 |
158 | const nextIndex = currentIndex + direction;
159 | if (nextIndex < 0 || nextIndex >= focusableElements.length) return null;
160 | return focusableElements[nextIndex];
161 | };
162 |
163 | const handlePickerTabNavigation = (event) => {
164 | if (event.defaultPrevented) return;
165 | if (!isPickerOpen || event.key !== "Tab") return;
166 | const picker = document.getElementById("clr-picker");
167 | if (!picker || !picker.contains(event.target)) return;
168 |
169 | const focusableElements = getPickerFocusableElements();
170 | if (!focusableElements.length) return;
171 |
172 | const firstFocusable = focusableElements[0];
173 | const lastFocusable = focusableElements[focusableElements.length - 1];
174 | const shouldExitForward = !event.shiftKey && event.target === lastFocusable;
175 | const shouldExitBackward = event.shiftKey && event.target === firstFocusable;
176 | if (!shouldExitForward && !shouldExitBackward) return;
177 |
178 | const direction = shouldExitBackward ? -1 : 1;
179 | const nextElement = getNextFocusableAfterTrigger(direction);
180 | if (!nextElement) return;
181 |
182 | event.preventDefault();
183 | pendingFocusTarget = nextElement;
184 | focusReturnTarget = null;
185 | Coloris.close();
186 | };
187 |
188 | const handlePickerEnterCommit = (event) => {
189 | if (event.defaultPrevented) return;
190 | if (!isPickerOpen || event.key !== "Enter") return;
191 | const picker = document.getElementById("clr-picker");
192 | if (!picker || !picker.contains(event.target)) return;
193 | if (event.target.tagName === "BUTTON") return;
194 | if (event.target.id === "clr-color-value") return;
195 | event.preventDefault();
196 | event.stopImmediatePropagation();
197 | event.stopPropagation();
198 | const closeButton = getPickerCloseButton();
199 | if (closeButton) {
200 | focusEl(closeButton);
201 | }
202 | };
203 |
204 | const handlePalettePickerClick = (event) => {
205 | const palettePickerButton = closestFromEvent(event, ".edit-base-button");
206 | if (!palettePickerButton) return;
207 | event.preventDefault();
208 | if (suppressNextPalettePickerOpen === palettePickerButton) {
209 | suppressNextPalettePickerOpen = null;
210 | return;
211 | }
212 | if (isPickerOpen && activePickerCell === palettePickerButton) {
213 | focusReturnTarget = palettePickerButton;
214 | suppressTooltipUntilMouseOut(palettePickerButton);
215 | Coloris.close();
216 | return;
217 | }
218 | const colorIndex = parseInt(palettePickerButton.getAttribute("data-color-index"), 10);
219 | const colorHex = normalizeHex(palettePickerButton.getAttribute("data-color-hex"));
220 | const rowType = palettePickerButton.getAttribute("data-row-type") || null;
221 | openPicker({
222 | target: palettePickerButton,
223 | baseHex: colorHex ? `#${colorHex}` : null,
224 | mode: "edit",
225 | index: colorIndex,
226 | rowType
227 | });
228 | };
229 |
230 | const handlePalettePickerPointerDown = (event) => {
231 | const palettePickerButton = closestFromEvent(event, ".edit-base-button");
232 | if (!palettePickerButton) return;
233 | if (!isPickerOpen || activePickerCell !== palettePickerButton) return;
234 | suppressNextPalettePickerOpen = palettePickerButton;
235 | focusReturnTarget = palettePickerButton;
236 | suppressTooltipUntilMouseOut(palettePickerButton);
237 | Coloris.close();
238 | event.preventDefault();
239 | event.stopPropagation();
240 | };
241 |
242 | if (!window[GLOBAL_GUARD]) {
243 | document.addEventListener("focusin", handlePickerFocusIn);
244 | document.addEventListener("keydown", handlePickerEscape);
245 | document.addEventListener("keydown", handlePickerTabNavigation, true);
246 | document.addEventListener("keydown", handlePickerEnterCommit, true);
247 | document.addEventListener("pointerdown", handlePalettePickerPointerDown);
248 | document.addEventListener("click", handlePalettePickerClick);
249 | window[GLOBAL_GUARD] = true;
250 | }
251 |
252 | const normalizeHex = (value) => {
253 | if (!value) return "";
254 | const raw = value.toString().trim();
255 | const withoutHash = raw.startsWith("#") ? raw.slice(1) : raw;
256 | const clean = withoutHash.replace(/[^0-9a-f]/gi, "").slice(0, 6).toLowerCase();
257 | if (clean.length === 3) {
258 | return clean.split("").map((char) => char + char).join("");
259 | }
260 | if (clean.length !== 6) return "";
261 | return clean;
262 | };
263 |
264 | const getThemeMode = () => (document.documentElement.classList.contains("darkmode-active") ? "dark" : "light");
265 | const WINDOW_REFRESH_HANDLER = "__tsColorPickerWindowRefreshHandler";
266 |
267 | let activePickerAnchor = null;
268 |
269 | const updatePickerInputPosition = () => {
270 | if (!pickerInput || !activePickerAnchor) return;
271 | const rect = activePickerAnchor.getBoundingClientRect();
272 | pickerInput.style.position = "fixed";
273 | pickerInput.style.left = `${rect.left}px`;
274 | pickerInput.style.top = `${rect.top}px`;
275 | pickerInput.style.width = `${rect.width}px`;
276 | pickerInput.style.height = `${rect.height}px`;
277 | pickerInput.style.pointerEvents = "none";
278 | pickerInput.style.opacity = "0";
279 | };
280 |
281 | const positionPickerInput = (target) => {
282 | if (!pickerInput || !target) return;
283 | activePickerAnchor = target;
284 | updatePickerInputPosition();
285 | };
286 |
287 | const refreshPickerPosition = () => {
288 | if (!activePickerAnchor) return;
289 | updatePickerInputPosition();
290 | };
291 |
292 | if (window[WINDOW_REFRESH_HANDLER]) {
293 | window.removeEventListener("resize", window[WINDOW_REFRESH_HANDLER]);
294 | window.removeEventListener("scroll", window[WINDOW_REFRESH_HANDLER]);
295 | }
296 | window.addEventListener("resize", refreshPickerPosition);
297 | window.addEventListener("scroll", refreshPickerPosition, { passive: true });
298 | window[WINDOW_REFRESH_HANDLER] = refreshPickerPosition;
299 |
300 | pickerInput.setAttribute("tabindex", "-1");
301 | pickerInput.setAttribute("aria-hidden", "true");
302 | pickerInput.inert = true;
303 |
304 | const setPickerBaseColor = (overrideHex) => {
305 | const parsedValues = window.palettes && window.palettes.parseColorValues
306 | ? window.palettes.parseColorValues(colorInput.value)
307 | : [];
308 | const lastHex = parsedValues && parsedValues.length ? parsedValues[parsedValues.length - 1] : null;
309 | const hexToUse = normalizeHex(overrideHex || (lastHex ? `#${lastHex}` : defaultColor));
310 | const formatted = `#${hexToUse || normalizeHex(defaultColor)}`;
311 | pickerInput.value = formatted;
312 | };
313 |
314 | const handlePalettePickerKeydown = (event) => {
315 | if (event.defaultPrevented) return;
316 | if (!isActivationKey(event)) return;
317 | const pickerCell = closestFromEvent(event, ".edit-base-button");
318 | if (!pickerCell) return;
319 | event.preventDefault();
320 | event.stopPropagation();
321 | suppressNextPalettePickerOpen = null;
322 | if (isPickerOpen && activePickerCell === pickerCell) {
323 | focusReturnTarget = pickerCell;
324 | suppressTooltipUntilMouseOut(pickerCell);
325 | Coloris.close();
326 | return;
327 | }
328 | const colorIndex = parseInt(pickerCell.getAttribute("data-color-index"), 10);
329 | const colorHex = normalizeHex(pickerCell.getAttribute("data-color-hex"));
330 | const rowType = pickerCell.getAttribute("data-row-type") || null;
331 | openPicker({
332 | target: pickerCell,
333 | baseHex: colorHex ? `#${colorHex}` : null,
334 | mode: "edit",
335 | index: colorIndex,
336 | rowType
337 | });
338 | };
339 |
340 | const getActiveHex = () => {
341 | const colorValueInput = document.getElementById("clr-color-value");
342 | const fromColorValue = colorValueInput ? normalizeHex(colorValueInput.value) : "";
343 | const fromPickerInput = normalizeHex(pickerInput.value);
344 | return pendingHex || fromColorValue || fromPickerInput || "";
345 | };
346 |
347 | const triggerPaletteRebuild = (options = {}) => {
348 | const form = document.getElementById("color-entry-form");
349 | if (form) {
350 | form.dispatchEvent(new CustomEvent("submit", { bubbles: true, cancelable: true, detail: options }));
351 | }
352 | };
353 |
354 | const normalizePickerInputValue = () => {
355 | const colorValueInput = document.getElementById("clr-color-value");
356 | if (!colorValueInput) return "";
357 | const normalized = normalizeHex(colorValueInput.value);
358 | if (!normalized) return "";
359 | const formatted = `#${normalized}`;
360 | if (colorValueInput.value !== formatted) {
361 | colorValueInput.value = formatted;
362 | colorValueInput.dispatchEvent(new Event("input", { bubbles: true }));
363 | }
364 | return formatted;
365 | };
366 |
367 | const applyCommittedHex = (hexValue) => {
368 | if (!hexValue || hasCommittedThisSession) return;
369 | const parsed = window.palettes && window.palettes.parseColorValues
370 | ? window.palettes.parseColorValues(colorInput.value) || []
371 | : [];
372 |
373 | if (activeContext.mode === "edit" && Number.isInteger(activeContext.index)) {
374 | if (activeContext.index < 0 || activeContext.index >= parsed.length) return;
375 | }
376 |
377 | if (activeContext.mode === "edit" && Number.isInteger(activeContext.index)) {
378 | if (!parsed.length) return;
379 | parsed[activeContext.index] = hexValue;
380 | colorInput.value = parsed.join(" ");
381 | } else {
382 | const currentValue = colorInput.value.trim();
383 | colorInput.value = currentValue ? `${currentValue} ${hexValue}` : hexValue;
384 | }
385 |
386 | pendingHex = "";
387 | colorInput.dispatchEvent(new Event("input", { bubbles: true }));
388 | if (activeContext.mode === "edit") {
389 | const focusPickerContext = Number.isInteger(activeContext.index)
390 | ? {
391 | colorIndex: activeContext.index,
392 | rowType: activeContext.rowType || null
393 | }
394 | : null;
395 | triggerPaletteRebuild({ skipScroll: true, skipFocus: true, focusPickerContext });
396 | }
397 | hasCommittedThisSession = true;
398 | Coloris.close();
399 | };
400 |
401 | const wireCloseButton = () => {
402 | const closeButton = document.getElementById("clr-close");
403 | if (!closeButton || closeButton.dataset.hexCloseAttached) return;
404 | closeButton.dataset.hexCloseAttached = "true";
405 | closeButton.addEventListener("click", (event) => {
406 | event.preventDefault();
407 | shouldCommit = true;
408 | applyCommittedHex(getActiveHex());
409 | });
410 | wireHexInputEnterHandler();
411 | };
412 |
413 | const wireHexInputEnterHandler = () => {
414 | const colorValueInput = document.getElementById("clr-color-value");
415 | if (!colorValueInput) return;
416 | colorValueInput.setAttribute("spellcheck", "false");
417 | colorValueInput.setAttribute("autocomplete", "off");
418 | colorValueInput.setAttribute("autocapitalize", "off");
419 | colorValueInput.setAttribute("autocorrect", "off");
420 | if (colorValueInput.dataset.hexEnterAttached) return;
421 | colorValueInput.dataset.hexEnterAttached = "true";
422 | colorValueInput.addEventListener("keydown", (event) => {
423 | if (event.defaultPrevented) return;
424 | if (event.key !== "Enter" && event.key !== "NumpadEnter") return;
425 | event.preventDefault();
426 | event.stopPropagation();
427 | normalizePickerInputValue();
428 | const closeButton = document.getElementById("clr-close");
429 | focusEl(closeButton || pickerButton);
430 | });
431 | };
432 |
433 | const focusPickerTextInput = () => {
434 | const colorValueInput = document.getElementById("clr-color-value");
435 | if (!colorValueInput) return;
436 | focusEl(colorValueInput);
437 | if (typeof colorValueInput.select === "function") {
438 | colorValueInput.select();
439 | return;
440 | }
441 | if (typeof colorValueInput.setSelectionRange === "function") {
442 | colorValueInput.setSelectionRange(0, colorValueInput.value.length);
443 | }
444 | };
445 |
446 | const openPicker = ({ target, baseHex, mode, index, rowType = null }) => {
447 | activeContext = {
448 | mode,
449 | index: Number.isInteger(index) ? index : null,
450 | rowType: rowType || null
451 | };
452 | hasCommittedThisSession = false;
453 | shouldCommit = false;
454 | focusButtonOnEsc = false;
455 | focusReturnTarget = target || null;
456 | activatePickerCell(target);
457 | positionPickerInput(target || pickerButton);
458 | Coloris.setInstance("#color-picker-input", { themeMode: getThemeMode(), parent: "body" });
459 | setPickerBaseColor(baseHex);
460 | pendingHex = "";
461 | isPickerOpen = true;
462 | pendingFocusTarget = null;
463 | setTimeout(wireCloseButton, 0);
464 | setTimeout(focusPickerTextInput, 0);
465 | pickerInput.dispatchEvent(new Event("click", { bubbles: true }));
466 | };
467 |
468 | Coloris({
469 | el: "#color-picker-input",
470 | theme: "polaroid",
471 | themeMode: getThemeMode(),
472 | parent: "body",
473 | alpha: false,
474 | format: "hex",
475 | focusInput: true,
476 | selectInput: true,
477 | closeButton: false,
478 | wrap: false,
479 | margin: 6,
480 | defaultColor,
481 | onChange: (color) => {
482 | pendingHex = normalizeHex(color);
483 | }
484 | });
485 |
486 | if (!pickerInput.dataset.tsBound) {
487 | pickerInput.dataset.tsBound = "true";
488 | pickerInput.addEventListener("close", () => {
489 | if (shouldCommit && !hasCommittedThisSession) {
490 | applyCommittedHex(pendingHex || getActiveHex());
491 | }
492 | pendingHex = "";
493 | shouldCommit = false;
494 | activePickerAnchor = null;
495 | clearActivePickerCell();
496 | isPickerOpen = false;
497 | if (focusButtonOnEsc) {
498 | focusEl(focusReturnTarget || pickerButton);
499 | focusButtonOnEsc = false;
500 | focusReturnTarget = null;
501 | return;
502 | }
503 | if (pendingFocusTarget) {
504 | const target = pendingFocusTarget;
505 | pendingFocusTarget = null;
506 | setTimeout(() => focusEl(target), 0);
507 | focusReturnTarget = null;
508 | return;
509 | }
510 | if (focusReturnTarget) {
511 | focusEl(focusReturnTarget);
512 | focusReturnTarget = null;
513 | }
514 | });
515 | }
516 |
517 | if (!pickerButton.dataset.tsBound) {
518 | pickerButton.dataset.tsBound = "true";
519 | pickerButton.addEventListener("pointerdown", (event) => {
520 | if (!isPickerOpen || activeContext.mode !== "add") return;
521 | suppressNextPickerButtonOpen = true;
522 | focusReturnTarget = pickerButton;
523 | Coloris.close();
524 | event.preventDefault();
525 | event.stopPropagation();
526 | });
527 | pickerButton.addEventListener("click", () => {
528 | if (suppressNextPickerButtonOpen) {
529 | suppressNextPickerButtonOpen = false;
530 | return;
531 | }
532 | if (isPickerOpen && activeContext.mode === "add") {
533 | focusReturnTarget = pickerButton;
534 | Coloris.close();
535 | return;
536 | }
537 | openPicker({ target: pickerButton, baseHex: null, mode: "add", index: null });
538 | });
539 |
540 | pickerButton.addEventListener("keydown", (event) => {
541 | if (event.defaultPrevented) return;
542 | if (isActivationKey(event)) {
543 | event.preventDefault();
544 | if (isPickerOpen && activeContext.mode === "add") {
545 | focusReturnTarget = pickerButton;
546 | Coloris.close();
547 | return;
548 | }
549 | openPicker({ target: pickerButton, baseHex: null, mode: "add", index: null });
550 | return;
551 | }
552 | if (event.key === "Tab" && !event.shiftKey && submitButton) {
553 | event.preventDefault();
554 | focusEl(submitButton);
555 | } else if (event.key === "Tab" && event.shiftKey && colorInput) {
556 | event.preventDefault();
557 | focusEl(colorInput);
558 | }
559 | });
560 | }
561 |
562 |
563 | const paletteContainer = document.getElementById("tints-and-shades");
564 | if (paletteContainer && !paletteContainer.dataset.tsPickerKeydownBound) {
565 | paletteContainer.dataset.tsPickerKeydownBound = "true";
566 | paletteContainer.addEventListener("keydown", handlePalettePickerKeydown, true);
567 | }
568 |
569 | if (submitButton) {
570 | submitButton.addEventListener("keydown", (event) => {
571 | if (event.defaultPrevented) return;
572 | if (event.key === "Tab" && event.shiftKey) {
573 | event.preventDefault();
574 | focusEl(pickerButton);
575 | }
576 | });
577 | }
578 | })();
579 |
--------------------------------------------------------------------------------
/src/styles/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'variables' as *;
2 |
3 | *,
4 | *:before,
5 | *:after {
6 | box-sizing: border-box;
7 | }
8 |
9 | html {
10 | font-size: 62.5%;
11 | color-scheme: light;
12 |
13 | @include breakpoint('large') {
14 | font-size: 55%;
15 | }
16 | }
17 |
18 | html,
19 | body {
20 | background-color: $white;
21 | transition: background-color 220ms ease;
22 | }
23 |
24 | body {
25 | font-size: 1.8rem;
26 | line-height: 1.618;
27 | font-family: $font-primary;
28 | font-weight: normal;
29 | text-align: center;
30 | color: $black;
31 |
32 | &.modal-open {
33 | overflow: hidden;
34 | position: fixed;
35 | width: 100%;
36 | }
37 | }
38 |
39 | a {
40 | color: $black;
41 | text-decoration: none;
42 | border-bottom: 3px solid $black;
43 |
44 | &:hover {
45 | border-color: $magenta;
46 | }
47 | }
48 |
49 | .header {
50 | display: flex;
51 | justify-content: center;
52 | padding: 7.5rem 2rem 8rem;
53 | margin: auto;
54 |
55 | @at-root body:has(.docs) & {
56 | padding-bottom: 6rem;
57 | }
58 |
59 | @include breakpoint('large') {
60 | padding: 9rem 2rem 6rem;
61 | }
62 |
63 | @include breakpoint('small') {
64 | padding-bottom: 4rem;
65 | }
66 |
67 | .preface {
68 | position: absolute;
69 | top: 0;
70 | display: flex;
71 | justify-content: space-between;
72 | width: 100%;
73 | left: 0;
74 |
75 | .dark-mode-selector {
76 | position: relative;
77 | top: 1.5rem;
78 | left: 2rem;
79 | display: grid;
80 | height: 4.8rem;
81 | width: 4.8rem;
82 |
83 | @include breakpoint('large') {
84 | top: 2rem;
85 | }
86 |
87 | .theme-toggle {
88 | background-color: $gray-50;
89 | border: none;
90 | cursor: pointer;
91 | display: grid;
92 | padding: 0;
93 | border-radius: 50%;
94 |
95 | &:hover {
96 | background-color: $gray-100;
97 | }
98 |
99 | &:active {
100 | background-color: color-mix(in srgb, $gray-100 92%, $black);
101 | }
102 |
103 | &-icon {
104 | display: inline-flex;
105 | align-items: center;
106 | justify-content: center;
107 |
108 | svg {
109 | width: 2.2rem;
110 | height: 2.2rem;
111 | color: $black;
112 | }
113 |
114 | .icon-tabler-sun-high {
115 | display: none;
116 | }
117 |
118 | .icon-tabler-moon {
119 | display: inline-block;
120 | }
121 | }
122 | }
123 | }
124 |
125 | .announcement {
126 | position: absolute;
127 | left: 50%;
128 | top: 2.2rem;
129 | transform: translateX(-50%);
130 | padding: .5rem 2rem;
131 | border-radius: 999px;
132 | background-color: $gray-50;
133 | color: $black;
134 | font-size: 1.3rem;
135 | display: inline-flex;
136 | align-items: center;
137 | border: none;
138 | font-weight: 500;
139 | gap: .3rem;
140 | transition: none;
141 |
142 | @include breakpoint('large') {
143 | top: 3rem;
144 | }
145 |
146 | &:hover {
147 | background-color: $gray-100;
148 | }
149 |
150 | &:active {
151 | background-color: color-mix(in srgb, $gray-100 92%, $black);
152 | }
153 |
154 | &-header {
155 | text-transform: uppercase;
156 | }
157 |
158 | &-icon {
159 | display: flex;
160 |
161 | svg {
162 | width: 2rem;
163 | height: 2rem;
164 | }
165 | }
166 | }
167 |
168 | a.github-corner {
169 | border: none;
170 | display: flex;
171 | fill: $black;
172 | color: $white;
173 | clip-path: polygon(0 0, 100% 0, 100% 100%);
174 | outline-offset: -1px;
175 |
176 | &:focus-visible {
177 | clip-path: none;
178 | }
179 | }
180 | }
181 |
182 | h1 {
183 | margin: 0;
184 | position: relative;
185 | z-index: 1;
186 |
187 | a {
188 | background-image: linear-gradient(to right, $orange, $magenta);
189 | border-radius: $radius-main;
190 | margin-bottom: 0;
191 | font-size: 5.6rem;
192 | font-weight: 900;
193 | line-height: 1.1;
194 | color: $white;
195 | text-decoration: none;
196 | border: none;
197 | padding: 1rem 3rem;
198 | display: flex;
199 | justify-content: center;
200 | width: 72rem;
201 |
202 | @include breakpoint('large') {
203 | width: 100%;
204 | }
205 |
206 | @include breakpoint('medium') {
207 | font-size: 4.8rem;
208 | padding: 1rem 1.5rem;
209 | }
210 |
211 | @include breakpoint('small') {
212 | font-size: 3.6rem;
213 | }
214 | }
215 | }
216 | }
217 |
218 | .main {
219 | margin: 0 auto 10rem;
220 | padding: 0 2rem;
221 | max-width: 85rem;
222 | }
223 |
224 | .form {
225 |
226 | form {
227 | display: flex;
228 | flex-direction: column;
229 | align-items: center;
230 | justify-content: center;
231 | }
232 |
233 | &-labels {
234 | position: relative;
235 | display: flex;
236 | justify-content: center;
237 |
238 | .textarea-label {
239 | font-weight: normal;
240 | font-size: 2.4rem;
241 | line-height: 1.3;
242 |
243 | @include breakpoint('small') {
244 | font-size: 2rem;
245 | }
246 | }
247 |
248 | .form-warning {
249 | opacity: 0;
250 | visibility: hidden;
251 | background-color: $alert;
252 | color: $white;
253 | position: absolute;
254 | bottom: -1rem;
255 | font-size: 2rem;
256 | line-height: 2.4rem;
257 | display: block;
258 | padding: .75rem 1.5rem;
259 | border-radius: $radius-main;
260 | transition: opacity 200ms linear, bottom 250ms ease-out, visibility 0s linear 250ms;
261 | z-index: 1;
262 |
263 | &:after {
264 | content: "";
265 | position: absolute;
266 | left: calc(50% - 1rem);
267 | margin-top: 1.5rem;
268 | width: 2rem;
269 | height: 2rem;
270 | transform: rotate(45deg);
271 | background-color: $alert;
272 | z-index: -1;
273 | }
274 |
275 | &.visible {
276 | opacity: 1;
277 | visibility: visible;
278 | bottom: .2rem;
279 | transition: opacity 200ms linear, bottom 250ms ease-out, visibility 0s;
280 | }
281 | }
282 | }
283 |
284 | &-input {
285 | display: flex;
286 | max-width: 54rem;
287 | margin: 1rem auto 2rem;
288 | width: 100%;
289 | position: relative;
290 |
291 | textarea {
292 | padding: 1.6rem;
293 | min-height: 24rem;
294 | background-color: $gray-50;
295 | border: 1px solid $gray-200;
296 | word-spacing: .25rem;
297 | border-radius: $radius-main;
298 | resize: none;
299 | width: 100%;
300 | font-family: $font-monospace;
301 |
302 | @media (pointer: coarse) {
303 | font-size: 16px;
304 | }
305 | }
306 | }
307 |
308 | .make-button {
309 | padding: 1.6rem;
310 | width: 100%;
311 | max-width: 54rem;
312 | border: none;
313 | background-color: $black;
314 | color: $white;
315 | border-radius: $radius-main;
316 | line-height: inherit;
317 | cursor: pointer;
318 | font-size: 2rem;
319 | font-weight: 400;
320 | transition: background-color 150ms ease-in-out;
321 |
322 | &:hover {
323 | background-color: $magenta;
324 | }
325 |
326 | &:active {
327 | background-color: color-mix(in srgb, $magenta 85%, $white);
328 | }
329 | }
330 |
331 | .color-picker-wrapper {
332 | position: absolute;
333 | right: 1.2rem;
334 | bottom: 1.2rem;
335 | z-index: 2;
336 | width: 4.8rem;
337 | height: 4.8rem;
338 |
339 | .color-picker-button {
340 | width: 100%;
341 | height: 100%;
342 | display: inline-flex;
343 | align-items: center;
344 | justify-content: center;
345 | padding: 0;
346 | border: none;
347 | border-radius: 50%;
348 | background-color: $white;
349 | color: $black;
350 | box-shadow: $shadow-main;
351 | cursor: pointer;
352 | transition: opacity 150ms ease-in-out, transform 150ms ease-in-out;
353 |
354 | svg {
355 | width: 2.2rem;
356 | height: 2.2rem;
357 | display: flex;
358 | }
359 |
360 | &:active {
361 | transform: scale(.9);
362 | opacity: .75;
363 | }
364 | }
365 |
366 | .color-picker-input {
367 | position: absolute;
368 | inset: 0;
369 | opacity: 0;
370 | pointer-events: none;
371 | border: none;
372 | padding: 0;
373 | }
374 | }
375 | }
376 |
377 | .copy-indicator {
378 | display: grid;
379 | place-items: center;
380 | position: absolute;
381 | top: 50%;
382 | left: 50%;
383 | pointer-events: none;
384 | transition: opacity 150ms ease-in-out, transform 150ms ease-in-out;
385 | will-change: opacity, transform;
386 | color: $black;
387 |
388 | svg {
389 | width: 2rem;
390 | height: 2rem;
391 | }
392 |
393 | &-check {
394 | color: $success;
395 |
396 | svg {
397 | width: 2.8rem;
398 | height: 2.8rem;
399 | }
400 | }
401 | }
402 |
403 | [data-tooltip] {
404 | position: relative;
405 | overflow: visible;
406 | }
407 |
408 | @media (hover: hover) and (pointer: fine) {
409 | [data-tooltip]::after {
410 | content: attr(data-tooltip);
411 | position: absolute;
412 | bottom: calc(100% + .6rem);
413 | left: 50%;
414 | background-color: $gray-600;
415 | color: $white;
416 | padding: .6rem 1.2rem;
417 | border-radius: $radius-main;
418 | font-size: 1.4rem;
419 | font-weight: 400;
420 | line-height: 1.4;
421 | z-index: 30;
422 | pointer-events: none;
423 | will-change: opacity, transform;
424 | max-width: 20rem;
425 | width: max-content;
426 | white-space: normal;
427 | overflow-wrap: anywhere;
428 | opacity: 0;
429 | transform: translate(-50%, .5rem);
430 | transition: opacity 150ms ease, transform 150ms ease;
431 | transition-delay: 0ms;
432 | }
433 |
434 | [data-tooltip]:hover::after {
435 | opacity: 1;
436 | transform: translate(-50%, 0);
437 | transition-delay: 500ms;
438 | }
439 |
440 | [data-tooltip]:focus-visible::after {
441 | opacity: 1;
442 | transform: translate(-50%, 0);
443 | transition: none;
444 | transition-delay: 0ms;
445 | }
446 |
447 | [data-tooltip][data-tooltip-immediate]::after {
448 | transition: none;
449 | transition-delay: 0ms;
450 | }
451 |
452 | [data-tooltip][data-tooltip-suppressed]::after {
453 | display: none;
454 | }
455 | }
456 |
457 | @include breakpoint('large') {
458 | [data-tooltip]::after {
459 | display: none;
460 | }
461 | }
462 |
463 | .edit-base-button {
464 | &.is-picker-open[data-tooltip]::after {
465 | display: none;
466 | }
467 | }
468 |
469 | #clr-picker {
470 | box-shadow: $shadow-main;
471 |
472 | &:before {
473 | color: transparent;
474 | }
475 |
476 | input.clr-color {
477 | border-color: $gray-200;
478 | background-color: $gray-50;
479 | font-family: $font-monospace;
480 | }
481 | }
482 |
483 | .palettes {
484 | margin-top: 6rem;
485 |
486 | .palette-controls-wrapper {
487 | margin-bottom: 2.8rem;
488 | opacity: 1;
489 |
490 | &[hidden] {
491 | display: none !important;
492 | }
493 |
494 | .palette-controls {
495 | display: flex;
496 | flex-wrap: wrap;
497 | justify-content: space-between;
498 | gap: 1.2rem;
499 |
500 | .step-selector {
501 | display: flex;
502 |
503 | &-option {
504 | border-color: transparent;
505 | width: 4.5rem;
506 | background-color: $gray-50;
507 | color: $black;
508 | padding: 0;
509 | font-size: 1.6rem;
510 | font-weight: 500;
511 | cursor: pointer;
512 |
513 | @include breakpoint('small') {
514 | width: 4rem;
515 | }
516 |
517 | &:hover {
518 | background-color: $gray-100;
519 | }
520 |
521 | &:active {
522 | background-color: color-mix(in srgb, $gray-100 92%, $black);
523 | }
524 |
525 | &.is-active {
526 | background-color: $gray-200;
527 |
528 | &:active {
529 | background-color: color-mix(in srgb, $gray-200 92%, $black);
530 | }
531 | }
532 |
533 | &:first-child {
534 | border-radius: $radius-main 0 0 $radius-main;
535 | }
536 |
537 | &:last-child {
538 | border-radius: 0 $radius-main $radius-main 0;
539 | }
540 | }
541 | }
542 |
543 | .inline-actions {
544 | display: flex;
545 | gap: 1.2rem;
546 |
547 | @include breakpoint('small') {
548 | gap: .8rem;
549 | }
550 | }
551 |
552 | .external-actions {
553 | display: inline-flex;
554 | gap: 1.2rem;
555 |
556 | @include breakpoint('small') {
557 | gap: .8rem;
558 | }
559 | }
560 |
561 | .action-button {
562 | border: none;
563 | border-radius: $radius-main;
564 | padding: 1rem;
565 | cursor: pointer;
566 | background-color: $gray-50;
567 | color: $black;
568 | display: inline-flex;
569 |
570 | @include breakpoint('small') {
571 | padding: .75rem;
572 | }
573 |
574 | .icon {
575 | width: 2rem;
576 | height: 2rem;
577 | }
578 |
579 | &:hover {
580 | background-color: $gray-100;
581 | }
582 |
583 | &:active {
584 | background-color: color-mix(in srgb, $gray-100 92%, $black);
585 | }
586 |
587 | &.is-active {
588 | background-color: $gray-200;
589 |
590 | &:active {
591 | background-color: color-mix(in srgb, $gray-200 92%, $black);
592 | }
593 | }
594 | }
595 | }
596 | }
597 |
598 | #tints-and-shades {
599 | overflow-x: visible;
600 | width: 100%;
601 | outline: 0;
602 |
603 | .palette-wrapper {
604 | margin-bottom: 2rem;
605 | transition: opacity 220ms ease, margin-bottom 220ms ease, height 220ms ease, padding-top 220ms ease, padding-bottom 220ms ease;
606 | will-change: opacity, margin-bottom, height, padding-top, padding-bottom;
607 |
608 | &[data-entering="true"] {
609 | opacity: 0;
610 | visibility: hidden;
611 | }
612 |
613 | &-fading {
614 | opacity: 0;
615 | pointer-events: none;
616 | }
617 |
618 | &-collapsing {
619 | pointer-events: none;
620 | }
621 |
622 | .palette-titlebar {
623 | display: flex;
624 | align-items: center;
625 | justify-content: space-between;
626 | margin-bottom: 1.8rem;
627 | text-align: left;
628 |
629 | &-controls {
630 | display: flex;
631 | align-items: center;
632 | gap: 1.2rem;
633 | }
634 |
635 | &-name {
636 | font-size: 1.4rem;
637 | line-height: normal;
638 | font-weight: 500;
639 | color: $black;
640 | flex: 1 1 0;
641 | min-width: 0;
642 | margin-right: 1.2rem;
643 | overflow: hidden;
644 | text-overflow: ellipsis;
645 | white-space: nowrap;
646 | }
647 |
648 | &-action {
649 | border: none;
650 | background-color: transparent;
651 | cursor: pointer;
652 | display: inline-flex;
653 | color: $black;
654 | padding: 0;
655 |
656 | svg {
657 | width: 2rem;
658 | height: 2rem;
659 | }
660 | }
661 | }
662 |
663 | .palette-complement-dropdown {
664 | position: relative;
665 | display: inline-flex;
666 |
667 | &-toggle {
668 | position: relative;
669 | z-index: 2;
670 | }
671 |
672 | &-menu {
673 | display: none;
674 | position: absolute;
675 | top: calc(100% + .4rem);
676 | right: -1rem;
677 | z-index: 20;
678 | background-color: $white;
679 | border-radius: $radius-main;
680 | box-shadow: $shadow-main;
681 | white-space: nowrap;
682 | }
683 |
684 | &.is-open {
685 | .palette-complement-dropdown-menu {
686 | display: flex;
687 | flex-direction: column;
688 | }
689 | }
690 |
691 | &-item {
692 | border: none;
693 | background-color: transparent;
694 | color: $black;
695 | font-size: 1.4rem;
696 | line-height: 1.4;
697 | text-align: left;
698 | width: 100%;
699 | padding: .8rem 1.2rem;
700 | cursor: pointer;
701 | overflow: hidden;
702 |
703 | &:is(:hover, :focus-visible) {
704 | background-color: $gray-100;
705 | }
706 |
707 | &:active {
708 | background-color: color-mix(in srgb, $gray-100 92%, $black);
709 | }
710 |
711 | &:first-child {
712 | border-top-left-radius: $radius-main;
713 | border-top-right-radius: $radius-main;
714 | }
715 |
716 | &:last-child {
717 | border-bottom-left-radius: $radius-main;
718 | border-bottom-right-radius: $radius-main;
719 | }
720 | }
721 | }
722 |
723 | .palette-table {
724 | overflow-x: auto;
725 |
726 | table {
727 | width: max-content;
728 | min-width: 100%;
729 | border-collapse: collapse;
730 |
731 | .table-header td {
732 | font-size: 1.4rem;
733 | min-width: 6.5rem;
734 | padding: 0 0 .5rem;
735 | font-weight: 500;
736 | line-height: normal;
737 | }
738 |
739 | td.hex-value {
740 | text-align: center;
741 | padding: .5rem 0 2rem;
742 | text-transform: lowercase;
743 | line-height: normal;
744 | font-family: $font-monospace;
745 |
746 | code {
747 | font-size: 1.3rem;
748 | }
749 | }
750 |
751 | td.hex-color {
752 | height: 6.5rem;
753 | min-width: 7.5rem;
754 | cursor: pointer;
755 | position: relative;
756 | outline: 0;
757 | overflow: hidden;
758 |
759 | &.copy-locked {
760 | pointer-events: none;
761 | }
762 |
763 | .copy-indicator {
764 | height: 4rem;
765 | width: 4rem;
766 | opacity: 0;
767 | border-radius: $radius-main;
768 | box-shadow: $shadow-main;
769 | background-color: $white;
770 | transform: translate(-50%, -45%);
771 |
772 | &-check {
773 | transform: translate(-50%, -50%);
774 | opacity: 0;
775 | transition: opacity 150ms ease;
776 | }
777 | }
778 |
779 | &:is(:hover, :focus-visible) .copy-indicator-copy {
780 | opacity: 1;
781 | transform: translate(-50%, -50%);
782 | }
783 |
784 | &:not(.copied):active .copy-indicator-copy {
785 | opacity: .75;
786 | transform: translate(-50%, -50%) scale(.9);
787 | }
788 |
789 | &.copied {
790 | .copy-indicator-check {
791 | opacity: 1;
792 | }
793 |
794 | .copy-indicator-copy {
795 | opacity: 0;
796 | }
797 | }
798 |
799 | @media (pointer: coarse) {
800 | .copy-indicator-copy {
801 | display: none;
802 | }
803 | }
804 | }
805 | }
806 | }
807 | }
808 | }
809 | }
810 |
811 | .utility-dialog {
812 | position: relative;
813 | width: min(50rem, 100vw);
814 | max-height: 60rem;
815 | border: 1px solid $gray-200;
816 | border-radius: $radius-main;
817 | padding: 1.6rem;
818 | box-shadow: 0 20px 70px rgba(0, 0, 0, .25);
819 | background-color: $white;
820 | color: $black;
821 | overflow: hidden;
822 | transform-origin: center;
823 |
824 | &-header {
825 | display: flex;
826 | align-items: center;
827 | justify-content: space-between;
828 | margin-bottom: 2rem;
829 | padding: .5rem 0;
830 | }
831 |
832 | &-title {
833 | margin: 0;
834 | font-size: 2rem;
835 | font-weight: 500;
836 | }
837 |
838 | &-close {
839 | border: none;
840 | background-color: transparent;
841 | cursor: pointer;
842 | color: $black;
843 | padding: 0;
844 |
845 | span {
846 | display: flex;
847 | }
848 |
849 | svg {
850 | width: 2.8rem;
851 | height: 2.8rem;
852 | }
853 | }
854 |
855 | .utility-copy-button {
856 | width: 4.8rem;
857 | height: 4.8rem;
858 | padding: 0;
859 | border-radius: 50%;
860 | border: none;
861 | background-color: $white;
862 | color: $black;
863 | cursor: pointer;
864 | box-shadow: $shadow-main;
865 | position: relative;
866 | transition: background-color 150ms ease, transform 150ms ease, box-shadow 150ms ease;
867 |
868 | .copy-indicator-copy {
869 | opacity: 1;
870 | transform: translate(-50%, -50%) scale(1);
871 | }
872 |
873 | .copy-indicator-check {
874 | opacity: 0;
875 | transform: translate(-50%, -50%) scale(1.2);
876 | }
877 |
878 | &:active {
879 | opacity: .75;
880 | transform: scale(.9);
881 | }
882 |
883 | &[disabled] {
884 | pointer-events: none;
885 | }
886 |
887 | &.copied {
888 | .copy-indicator-copy {
889 | opacity: 0;
890 | transform: translate(-50%, -50%) scale(.8);
891 | }
892 |
893 | .copy-indicator-check {
894 | opacity: 1;
895 | transform: translate(-50%, -50%) scale(1);
896 | }
897 | }
898 | }
899 |
900 | &:not([open]) {
901 | display: none;
902 | }
903 |
904 | &[open] {
905 | display: flex;
906 | flex-direction: column;
907 | height: 90vh;
908 | }
909 |
910 | &::backdrop {
911 | background-color: rgba(0, 0, 0, .5);
912 | opacity: 1;
913 | }
914 |
915 | &.is-opening {
916 | animation: export-dialog-fade 180ms ease-out forwards;
917 |
918 | &::backdrop {
919 | animation: export-backdrop-fade 180ms ease-out forwards;
920 | }
921 | }
922 |
923 | &.is-closing {
924 | animation: export-dialog-fade 150ms ease-in reverse forwards;
925 |
926 | &::backdrop {
927 | animation: export-backdrop-fade 150ms ease-in reverse forwards;
928 | }
929 | }
930 |
931 | &.export-dialog {
932 | .export-tabs {
933 | display: flex;
934 | z-index: 1;
935 |
936 | .export-tab {
937 | background-color: transparent;
938 | color: $gray-400;
939 | padding: .8rem 1.6rem;
940 | cursor: pointer;
941 | font-weight: 500;
942 | border: 1px solid transparent;
943 | border-radius: $radius-main $radius-main 0 0;
944 | border-bottom: none;
945 | font-size: 1.6rem;
946 | user-select: none;
947 |
948 | &:hover {
949 | color: $black;
950 | }
951 |
952 | &.is-active {
953 | color: $black;
954 | background-color: $gray-50;
955 | }
956 | }
957 | }
958 |
959 | .export-body {
960 | display: flex;
961 | flex: 1 1 auto;
962 | min-height: 0;
963 |
964 | .export-output {
965 | margin: 0;
966 | padding: 1.8rem;
967 | flex: 1 1 auto;
968 | border-radius: $radius-main;
969 | background-color: $gray-50;
970 |
971 | &.is-first-tab-active {
972 | border-radius: 0 $radius-main $radius-main $radius-main;
973 | }
974 |
975 | &-code {
976 | display: block;
977 | width: 100%;
978 | text-shadow: none;
979 | font-size: 1.4rem;
980 | font-family: $font-monospace;
981 | color: $black;
982 | margin: -2.7rem 0 -5.4rem;
983 |
984 | html:not(.darkmode-active) & {
985 |
986 | .token.selector,
987 | .token.string {
988 | color: #4f7300;
989 | }
990 |
991 | .token.comment {
992 | color: #5e6f80;
993 | }
994 |
995 | .token.operator {
996 | background-color: transparent;
997 | }
998 | }
999 | }
1000 | }
1001 | }
1002 |
1003 | .export-copy {
1004 | position: absolute;
1005 | right: 3rem;
1006 | bottom: 3rem;
1007 | display: flex;
1008 | }
1009 | }
1010 |
1011 | &.share-dialog {
1012 | .share-body {
1013 | display: flex;
1014 | flex-direction: column;
1015 | align-items: flex-end;
1016 | gap: 1.5rem;
1017 | margin-bottom: .5rem;
1018 |
1019 | .share-input {
1020 | width: 100%;
1021 | font-size: 1.4rem;
1022 | line-height: 1.5;
1023 | padding: 1.2rem 1.4rem;
1024 | border-radius: $radius-main;
1025 | border: none;
1026 | background-color: $gray-50;
1027 | font-family: $font-monospace;
1028 | resize: none;
1029 | min-height: 4.5rem;
1030 | color: $black;
1031 | }
1032 | }
1033 |
1034 | &[open] {
1035 | height: fit-content;
1036 | }
1037 | }
1038 | }
1039 |
1040 | .footer ul {
1041 | list-style: none;
1042 | display: flex;
1043 | gap: 4rem;
1044 | justify-content: center;
1045 | padding: 0;
1046 | margin: 0 0 8rem;
1047 | }
1048 |
1049 | .sr-only,
1050 | .skip-link {
1051 | position: absolute;
1052 | width: 1px;
1053 | height: 1px;
1054 | padding: 0;
1055 | margin: -1px;
1056 | overflow: hidden;
1057 | clip-path: inset(50%);
1058 | white-space: nowrap;
1059 | border: 0;
1060 | }
1061 |
1062 | .skip-link {
1063 | &:focus-visible {
1064 | top: 1rem;
1065 | left: 1rem;
1066 | width: auto;
1067 | height: auto;
1068 | margin: 0;
1069 | overflow: visible;
1070 | clip-path: none;
1071 | white-space: normal;
1072 | display: inline-block;
1073 | padding: 1rem 2rem;
1074 | border-radius: $radius-main;
1075 | color: $white;
1076 | background-color: $black;
1077 | z-index: 1;
1078 | transition: none;
1079 | }
1080 | }
1081 |
1082 | @keyframes export-dialog-fade {
1083 | from {
1084 | opacity: 0;
1085 | transform: translateY(12px) scale(.98);
1086 | }
1087 |
1088 | to {
1089 | opacity: 1;
1090 | transform: translateY(0) scale(1);
1091 | }
1092 | }
1093 |
1094 | @keyframes export-backdrop-fade {
1095 | from {
1096 | opacity: 0;
1097 | }
1098 |
1099 | to {
1100 | opacity: 1;
1101 | }
1102 | }
1103 |
1104 | @media (prefers-reduced-motion: reduce) {
1105 |
1106 | .utility-dialog.is-opening,
1107 | .utility-dialog.is-closing,
1108 | .utility-dialog.is-opening::backdrop,
1109 | .utility-dialog.is-closing::backdrop {
1110 | animation: none;
1111 | }
1112 | }
1113 |
1114 | .docs {
1115 | text-align: left;
1116 |
1117 | h2 {
1118 | margin: 3.6rem 0 0;
1119 | font-size: 2.7rem;
1120 | line-height: 1.3;
1121 | font-weight: bold;
1122 |
1123 | &:first-child {
1124 | margin-top: 0;
1125 | }
1126 | }
1127 |
1128 | h3 {
1129 | margin: 2rem 0 -.5rem;
1130 | }
1131 |
1132 | p,
1133 | ul,
1134 | ol {
1135 | margin: 1.5rem 0;
1136 | }
1137 |
1138 | li {
1139 | margin-bottom: .5rem;
1140 | }
1141 |
1142 | code {
1143 | background-color: $gray-100;
1144 | font-size: 90%;
1145 | padding: .2rem .6rem;
1146 | border-radius: $radius-main;
1147 | font-family: $font-monospace;
1148 | }
1149 |
1150 | .anchorjs-link {
1151 | border: none;
1152 |
1153 | &:hover {
1154 | color: $magenta;
1155 | }
1156 | }
1157 |
1158 | .subheader-icon {
1159 | vertical-align: middle;
1160 |
1161 | svg {
1162 | width: 2.4rem;
1163 | height: 2.4rem;
1164 | }
1165 | }
1166 | }
1167 |
1168 | .not-found {
1169 | background-color: $gray-50;
1170 | color: $black;
1171 | display: inline-block;
1172 | padding: 4rem 8rem;
1173 | border-radius: $radius-main;
1174 |
1175 | h2 {
1176 | font-weight: 900;
1177 | font-size: 6rem;
1178 | line-height: 1;
1179 | margin: 0;
1180 | }
1181 | }
--------------------------------------------------------------------------------
/src/js/export-ui.js:
--------------------------------------------------------------------------------
1 | (() => {
2 | const VALID_EXPORT_FORMATS = ["hex", "hex-hash", "rgb", "css", "json"];
3 | const EXPORT_FORMAT_STORAGE_KEY = "export-preferred-format";
4 |
5 | const getStoredExportFormat = () => {
6 | try {
7 | const storedFormat = localStorage.getItem(EXPORT_FORMAT_STORAGE_KEY);
8 | if (storedFormat && VALID_EXPORT_FORMATS.includes(storedFormat)) {
9 | return storedFormat;
10 | }
11 | } catch (err) {
12 | // Ignore storage errors and fall back to default
13 | }
14 | return "hex";
15 | };
16 |
17 | const persistExportFormat = (format) => {
18 | if (!VALID_EXPORT_FORMATS.includes(format)) return;
19 | try {
20 | localStorage.setItem(EXPORT_FORMAT_STORAGE_KEY, format);
21 | } catch (err) {
22 | // Ignore storage errors
23 | }
24 | };
25 |
26 | const exportState = {
27 | palettes: [],
28 | format: getStoredExportFormat()
29 | };
30 |
31 | const exportElements = {
32 | wrapper: null,
33 | openButton: null,
34 | modal: null,
35 | closeButton: null,
36 | tabs: [],
37 | output: null,
38 | code: null,
39 | copyFab: null,
40 | copyStatus: null,
41 | imageButton: null,
42 | };
43 |
44 | const LANGUAGE_CLASSES = [
45 | "language-none",
46 | "language-css",
47 | "language-json",
48 | "language-javascript",
49 | "language-markup"
50 | ];
51 |
52 | const getLanguageClassForFormat = (format) => {
53 | switch (format) {
54 | case "css":
55 | return "language-css";
56 | case "json":
57 | return "language-json";
58 | case "js":
59 | return "language-javascript";
60 | case "html":
61 | return "language-markup";
62 | default:
63 | return "language-none";
64 | }
65 | };
66 |
67 | const updateExportLanguage = (format, elements) => {
68 | if (!elements.output) return;
69 | const languageClass = getLanguageClassForFormat(format);
70 | LANGUAGE_CLASSES.forEach((className) => {
71 | elements.output.classList.remove(className);
72 | if (elements.code) {
73 | elements.code.classList.remove(className);
74 | }
75 | });
76 | elements.output.classList.add(languageClass);
77 | if (elements.code) {
78 | elements.code.classList.add(languageClass);
79 | }
80 | };
81 |
82 | const highlightExportCode = (codeElement) => {
83 | if (!codeElement || typeof window === "undefined") return;
84 | const prism = window.Prism;
85 | if (!prism || typeof prism.highlightElement !== "function") return;
86 | prism.highlightElement(codeElement);
87 | };
88 |
89 | let pageScrollY = 0;
90 | let handleOutsidePointerDown = null;
91 |
92 | const clampPercent = (percent) => {
93 | if (typeof percent !== "number" || Number.isNaN(percent)) return 0;
94 | return Math.min(Math.max(percent, 0), 100);
95 | };
96 |
97 | const formatCssTier = (percent) => {
98 | const safe = clampPercent(percent);
99 | return Math.round(safe * 10).toString(); // use 100s scale (10% => 100)
100 | };
101 |
102 | const prefersReducedMotion = () => {
103 | return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
104 | };
105 |
106 | const canvasToBlob = (canvas) => new Promise((resolve) => {
107 | const serialize = () => {
108 | const dataUrl = canvas.toDataURL("image/png");
109 | const base64 = dataUrl.split(",")[1] || "";
110 | const binary = atob(base64);
111 | const buffer = new Uint8Array(binary.length);
112 | for (let i = 0; i < binary.length; i += 1) {
113 | buffer[i] = binary.charCodeAt(i);
114 | }
115 | resolve(new Blob([buffer], { type: "image/png" }));
116 | };
117 |
118 | if (canvas.toBlob) {
119 | canvas.toBlob((blob) => {
120 | if (blob) {
121 | resolve(blob);
122 | return;
123 | }
124 | serialize();
125 | });
126 | return;
127 | }
128 | serialize();
129 | });
130 |
131 | const downloadBlob = (blob, filename) => {
132 | if (!blob) return;
133 | const url = URL.createObjectURL(blob);
134 | const link = document.createElement("a");
135 | link.href = url;
136 | link.download = filename;
137 | document.body.appendChild(link);
138 | link.click();
139 | link.remove();
140 | setTimeout(() => URL.revokeObjectURL(url), 1000);
141 | };
142 |
143 | function createTableCanvas(table) {
144 | if (!table) return null;
145 |
146 | const rows = Array.from(table.rows || []);
147 | if (!rows.length) return null;
148 |
149 | const tableRect = table.getBoundingClientRect();
150 | const tableStyle = window.getComputedStyle(table);
151 |
152 | const totalWidth = Math.max(1, Math.round(tableRect.width));
153 | const totalHeight = Math.max(1, Math.round(tableRect.height));
154 | if (!totalWidth || !totalHeight) return null;
155 |
156 | const paddingX = 24;
157 | const paddingY = 16;
158 | const bottomTrim = 12;
159 |
160 | const ratio = Math.max(1, window.devicePixelRatio || 1);
161 | const canvasWidthCss = totalWidth + paddingX * 2;
162 | const canvasHeightCss = totalHeight + paddingY * 2 - bottomTrim;
163 |
164 | const canvas = document.createElement("canvas");
165 | canvas.width = Math.max(1, Math.round(canvasWidthCss * ratio));
166 | canvas.height = Math.max(1, Math.round(canvasHeightCss * ratio));
167 |
168 | const ctx = canvas.getContext("2d");
169 | if (!ctx) return null;
170 |
171 | ctx.scale(ratio, ratio);
172 |
173 | const rootBackground = window.getComputedStyle(document.documentElement).backgroundColor;
174 | const isTransparent = (value) =>
175 | !value || value === "transparent" || value === "rgba(0, 0, 0, 0)";
176 |
177 | let tableBackground = tableStyle.backgroundColor;
178 | if (isTransparent(tableBackground)) {
179 | tableBackground = !isTransparent(rootBackground) ? rootBackground : "#fff";
180 | }
181 |
182 | ctx.fillStyle = tableBackground;
183 | ctx.fillRect(0, 0, canvasWidthCss, canvasHeightCss);
184 |
185 | ctx.textBaseline = "top";
186 |
187 | rows.forEach((row) => {
188 | const rowRect = row.getBoundingClientRect();
189 | const rowTop = Math.round(rowRect.top - tableRect.top);
190 | const rowHeight = Math.max(1, Math.round(rowRect.height));
191 |
192 | const cells = Array.from(row.cells);
193 | if (!cells.length) return;
194 |
195 | const cellRects = cells.map((cell) => cell.getBoundingClientRect());
196 | const edges = cellRects.map((r) => ({
197 | left: r.left - tableRect.left,
198 | right: r.right - tableRect.left
199 | }));
200 |
201 | const intEdges = edges.map((edge, index) => {
202 | const left = Math.round(edge.left);
203 | let right = Math.round(edge.right);
204 | if (index === edges.length - 1) right = totalWidth;
205 | return { left, right, width: Math.max(1, right - left) };
206 | });
207 |
208 | cells.forEach((cell, index) => {
209 | const { left, width } = intEdges[index];
210 | const x = paddingX + left;
211 | const y = paddingY + rowTop;
212 |
213 | const computed = window.getComputedStyle(cell);
214 | const cellBg = computed.backgroundColor;
215 |
216 | if (!isTransparent(cellBg)) {
217 | ctx.fillStyle = cellBg;
218 | } else {
219 | ctx.fillStyle = tableBackground;
220 | }
221 | const isLastCell = index === cells.length - 1;
222 | ctx.fillRect(
223 | x,
224 | y,
225 | isLastCell ? width : width + 2,
226 | rowHeight
227 | );
228 |
229 | const rawText = (cell.textContent || "").trim();
230 | const hexPattern = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
231 | let text = rawText;
232 | if (hexPattern.test(rawText)) {
233 | text = rawText.toLowerCase();
234 | }
235 | if (!text) return;
236 |
237 | ctx.fillStyle = computed.color || "#000";
238 | const fontSize = 14;
239 | const fontFamily = computed.fontFamily || "Work Sans, system-ui, sans-serif";
240 | ctx.font = `${fontSize}px ${fontFamily}`;
241 |
242 | const isHeaderRow = row.classList && row.classList.contains("table-header");
243 | const isNameRow = row.classList && row.classList.contains("palette-titlebar");
244 | const titleOffset = isHeaderRow ? 8 : 8;
245 |
246 | const textAlignValue = computed.textAlign || "left";
247 | const direction = computed.direction || "ltr";
248 | let normalizedAlign = textAlignValue;
249 |
250 | if (textAlignValue === "start") {
251 | normalizedAlign = direction === "rtl" ? "right" : "left";
252 | } else if (textAlignValue === "end") {
253 | normalizedAlign = direction === "rtl" ? "left" : "right";
254 | }
255 |
256 | if (isNameRow) {
257 | normalizedAlign = "left";
258 | }
259 |
260 | ctx.textAlign = normalizedAlign;
261 |
262 | let textX = x + width / 2;
263 | if (normalizedAlign === "left") {
264 | const paddingLeft = parseFloat(computed.paddingLeft) || 0;
265 | textX = x + paddingLeft;
266 | } else if (normalizedAlign === "right") {
267 | const paddingRight = parseFloat(computed.paddingRight) || 0;
268 | textX = x + width - paddingRight;
269 | }
270 |
271 | ctx.fillText(text, textX, y + titleOffset);
272 | });
273 | });
274 |
275 | return { canvas, width: totalWidth, height: totalHeight };
276 | }
277 |
278 | const downloadPaletteTableAsPng = async () => {
279 | if (!exportState.palettes || !exportState.palettes.length) return;
280 | const tableWrapper = document.getElementById("tints-and-shades");
281 | if (!tableWrapper) return;
282 | const paletteWrappers = Array.from(tableWrapper.querySelectorAll(".palette-wrapper"));
283 | if (!paletteWrappers.length) return;
284 | const aggregatedTable = document.createElement("table");
285 | const firstTable = paletteWrappers[0].querySelector("table");
286 | if (firstTable) aggregatedTable.className = firstTable.className;
287 |
288 | const copyComputedProperties = (sourceElement, targetElement, properties) => {
289 | if (!sourceElement || !targetElement) return;
290 | const computed = window.getComputedStyle(sourceElement);
291 | properties.forEach((property) => {
292 | const value = computed.getPropertyValue(property);
293 | if (value !== "") {
294 | targetElement.style.setProperty(property, value);
295 | }
296 | });
297 | };
298 |
299 | const copyRowStyles = (sourceRow, targetRow) => {
300 | Array.from(sourceRow.cells).forEach((cell, index) => {
301 | const targetCell = targetRow.cells[index];
302 | if (!targetCell) return;
303 | copyComputedProperties(cell, targetCell, [
304 | "background-color",
305 | "color",
306 | "font-family",
307 | "font-size",
308 | "font-weight",
309 | "line-height",
310 | "letter-spacing",
311 | "text-align",
312 | "direction",
313 | "text-transform",
314 | "padding-left",
315 | "padding-right",
316 | "padding-top",
317 | "padding-bottom",
318 | "box-sizing"
319 | ]);
320 | const cellRect = cell.getBoundingClientRect();
321 | targetCell.style.setProperty("width", `${Math.max(1, Math.round(cellRect.width))}px`);
322 | targetCell.style.setProperty("height", `${Math.max(1, Math.round(cellRect.height))}px`);
323 | });
324 | };
325 |
326 | paletteWrappers.forEach((wrapper, index) => {
327 | const paletteNameLabel = wrapper.querySelector(".palette-titlebar-name");
328 | const paletteNameText = (paletteNameLabel && paletteNameLabel.textContent)
329 | ? paletteNameLabel.textContent.trim()
330 | : "";
331 | const innerTable = wrapper.querySelector("table");
332 | if (!innerTable) return;
333 | const headerRow = innerTable.querySelector(".table-header");
334 | const columnCount = headerRow
335 | ? headerRow.cells.length
336 | : (innerTable.rows[0] ? innerTable.rows[0].cells.length : 1);
337 |
338 | if (paletteNameText) {
339 | const nameRow = document.createElement("tr");
340 | nameRow.className = "palette-titlebar";
341 | const nameCell = document.createElement("td");
342 | nameCell.setAttribute("colspan", `${columnCount}`);
343 | nameCell.textContent = paletteNameText;
344 | nameCell.style.paddingBottom = "8px";
345 | if (paletteNameLabel) {
346 | copyComputedProperties(paletteNameLabel, nameCell, [
347 | "color",
348 | "font-family",
349 | "font-size",
350 | "font-weight",
351 | "letter-spacing",
352 | "text-transform"
353 | ]);
354 | }
355 | nameRow.appendChild(nameCell);
356 | aggregatedTable.appendChild(nameRow);
357 | }
358 |
359 | if (headerRow) {
360 | const clonedHeader = headerRow.cloneNode(true);
361 | copyRowStyles(headerRow, clonedHeader);
362 | Array.from(clonedHeader.cells).forEach((cell) => {
363 | cell.style.paddingBottom = "4px";
364 | cell.style.paddingTop = "6px";
365 | cell.style.verticalAlign = "bottom";
366 | cell.style.lineHeight = "1.2";
367 | });
368 | aggregatedTable.appendChild(clonedHeader);
369 | }
370 |
371 | innerTable.querySelectorAll("tbody tr").forEach((row) => {
372 | const clonedRow = row.cloneNode(true);
373 | copyRowStyles(row, clonedRow);
374 | aggregatedTable.appendChild(clonedRow);
375 | });
376 |
377 | const isLastPalette = index === paletteWrappers.length - 1;
378 | if (!isLastPalette) {
379 | const spacerRow = document.createElement("tr");
380 | const spacerCell = document.createElement("td");
381 | spacerCell.setAttribute("colspan", `${columnCount}`);
382 | spacerCell.innerHTML = " ";
383 | spacerCell.style.height = "8px";
384 | spacerCell.style.lineHeight = "8px";
385 | spacerCell.style.border = "none";
386 | spacerRow.appendChild(spacerCell);
387 | aggregatedTable.appendChild(spacerRow);
388 | }
389 | });
390 |
391 | const rowCount = aggregatedTable.rows.length;
392 | if (!rowCount) return;
393 |
394 | const hiddenWrapper = document.createElement("div");
395 | Object.assign(hiddenWrapper.style, {
396 | position: "absolute",
397 | top: "-9999px",
398 | left: "-9999px",
399 | opacity: "0",
400 | pointerEvents: "none",
401 | });
402 | const paletteTableShim = document.createElement("div");
403 | paletteTableShim.className = "palette-table";
404 | paletteTableShim.appendChild(aggregatedTable);
405 | hiddenWrapper.appendChild(paletteTableShim);
406 | tableWrapper.appendChild(hiddenWrapper);
407 |
408 | let canvasResult = null;
409 | try {
410 | canvasResult = createTableCanvas(aggregatedTable);
411 | } finally {
412 | if (hiddenWrapper.parentNode) {
413 | hiddenWrapper.parentNode.removeChild(hiddenWrapper);
414 | }
415 | }
416 | if (!canvasResult || !canvasResult.canvas) return;
417 | const { canvas } = canvasResult;
418 | const imageButton = exportElements.imageButton;
419 | if (imageButton) {
420 | imageButton.disabled = true;
421 | imageButton.setAttribute("aria-busy", "true");
422 | }
423 | try {
424 | const blob = await canvasToBlob(canvas);
425 | if (!blob) throw new Error("Failed to create PNG");
426 | const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
427 | downloadBlob(blob, `palettes-${timestamp}.png`);
428 | } catch (error) {
429 | console.error("Failed to export palette table as PNG", error);
430 | } finally {
431 | if (imageButton) {
432 | imageButton.disabled = false;
433 | imageButton.removeAttribute("aria-busy");
434 | }
435 | }
436 | };
437 |
438 | const lockBodyScroll = () => {
439 | if (document.body.classList.contains("modal-open")) return;
440 | pageScrollY = window.scrollY || document.documentElement.scrollTop || 0;
441 | document.body.style.position = "fixed";
442 | document.body.style.top = `-${pageScrollY}px`;
443 | document.body.style.left = "0";
444 | document.body.style.right = "0";
445 | document.body.style.width = "100%";
446 | document.body.classList.add("modal-open");
447 | };
448 |
449 | const unlockBodyScroll = () => {
450 | const scrollToY = pageScrollY || 0;
451 | document.body.classList.remove("modal-open");
452 | document.body.style.position = "";
453 | document.body.style.top = "";
454 | document.body.style.left = "";
455 | document.body.style.right = "";
456 | document.body.style.width = "";
457 | window.scrollTo(0, scrollToY);
458 | };
459 |
460 | const getDialogFocusable = () => {
461 | if (!exportElements.modal) return [];
462 | const selectors = [
463 | "button",
464 | "[href]",
465 | 'input:not([type=\"hidden\"])',
466 | "select",
467 | "textarea",
468 | "[tabindex]:not([tabindex='-1'])"
469 | ];
470 | const nodes = Array.from(exportElements.modal.querySelectorAll(selectors.join(",")));
471 | return nodes.filter((node) => {
472 | const tabIndex = node.tabIndex;
473 | const isHidden = node.getAttribute("aria-hidden") === "true";
474 | const isDisabled = node.hasAttribute("disabled");
475 | return !isHidden && !isDisabled && tabIndex !== -1;
476 | });
477 | };
478 |
479 | const resetExportScroll = (elements) => {
480 | if (elements.output) {
481 | elements.output.scrollTop = 0;
482 | elements.output.scrollLeft = 0;
483 | }
484 | if (elements.output && elements.output.parentElement) {
485 | elements.output.parentElement.scrollTop = 0;
486 | elements.output.parentElement.scrollLeft = 0;
487 | }
488 | };
489 |
490 | const formatHexOutput = (palettes, includeHash, stepLabel) => {
491 | if (!palettes.length) return "";
492 | const prefix = includeHash ? "#" : "";
493 | const shadesHeader = stepLabel ? `${stepLabel} shades` : "shades";
494 | const tintsHeader = stepLabel ? `${stepLabel} tints` : "tints";
495 | const blocks = palettes.map((palette) => {
496 | const rawLabel = palette.label || palette.id;
497 | const label = exportNaming.formatLabelForDisplay(rawLabel) || rawLabel;
498 | const baseLine = `${label} - ${prefix}${palette.base}`;
499 | const shadeLines = palette.shades.map((item) => `${prefix}${item.hex}`);
500 | const tintLines = palette.tints.map((item) => `${prefix}${item.hex}`);
501 | return [
502 | baseLine,
503 | "",
504 | shadesHeader,
505 | "-----",
506 | ...shadeLines,
507 | "",
508 | tintsHeader,
509 | "-----",
510 | ...tintLines
511 | ].join("\n");
512 | });
513 | return blocks.join("\n\n");
514 | };
515 |
516 | const formatCssOutput = (palettes) => {
517 | if (!palettes.length) return "";
518 | const lines = [];
519 | palettes.forEach((palette) => {
520 | const baseName = palette.id;
521 | const rawLabel = palette.label || baseName;
522 | const label = exportNaming.formatLabelForDisplay(rawLabel) || rawLabel;
523 | lines.push(` /* ${label} */`);
524 | lines.push(` --${baseName}-base: #${palette.base};`);
525 | const shadeLines = palette.shades.map((item) => {
526 | const tier = formatCssTier(item.percent);
527 | return ` --${baseName}-shade-${tier}: #${item.hex};`;
528 | });
529 | const tintLines = palette.tints.map((item) => {
530 | const tier = formatCssTier(item.percent);
531 | return ` --${baseName}-tint-${tier}: #${item.hex};`;
532 | });
533 | lines.push(...shadeLines, ...tintLines);
534 | });
535 | return `:root {\n${lines.join("\n")}\n}`;
536 | };
537 |
538 | const formatJsonOutput = (palettes) => {
539 | if (!palettes.length) return "";
540 | const makePalettePayload = (palette) => {
541 | const shades = palette.shades.map((item) => {
542 | const step = formatCssTier(item.percent);
543 | return {
544 | step: Number(step),
545 | name: `${palette.id}-shade-${step}`,
546 | hex: `#${item.hex}`
547 | };
548 | });
549 | const tints = palette.tints.map((item) => {
550 | const step = formatCssTier(item.percent);
551 | return {
552 | step: Number(step),
553 | name: `${palette.id}-tint-${step}`,
554 | hex: `#${item.hex}`
555 | };
556 | });
557 | return {
558 | base: {
559 | name: palette.id,
560 | hex: `#${palette.base}`
561 | },
562 | shades,
563 | tints
564 | };
565 | };
566 |
567 | if (palettes.length === 1) {
568 | return JSON.stringify(makePalettePayload(palettes[0]), null, 2);
569 | }
570 |
571 | const payload = {};
572 | palettes.forEach((palette) => {
573 | payload[palette.id] = makePalettePayload(palette);
574 | });
575 | return JSON.stringify(payload, null, 2);
576 | };
577 |
578 | const normalizeHex = (hex) => {
579 | if (typeof hex !== "string") return "";
580 | return hex.replace(/^#/, "").trim().slice(0, 6);
581 | };
582 |
583 | const formatRgbValue = (hex) => {
584 | const normalized = normalizeHex(hex);
585 | if (normalized.length !== 6) return "";
586 | const r = parseInt(normalized.slice(0, 2), 16);
587 | const g = parseInt(normalized.slice(2, 4), 16);
588 | const b = parseInt(normalized.slice(4, 6), 16);
589 | if ([r, g, b].some((value) => Number.isNaN(value))) return "";
590 | return `rgb(${r}, ${g}, ${b})`;
591 | };
592 |
593 | const formatRgbOutput = (palettes, stepLabel) => {
594 | if (!palettes.length) return "";
595 | const blocks = palettes.map((palette) => {
596 | const rawLabel = palette.label || palette.id;
597 | const label = exportNaming.formatLabelForDisplay(rawLabel) || rawLabel;
598 | const rgbValue = formatRgbValue(palette.base) || palette.base;
599 | const baseLine = `${label} - ${rgbValue}`;
600 | const shadeLines = palette.shades.map((item) => formatRgbValue(item.hex) || item.hex);
601 | const tintLines = palette.tints.map((item) => formatRgbValue(item.hex) || item.hex);
602 | const shadesHeader = stepLabel ? `${stepLabel} shades` : "shades";
603 | const tintsHeader = stepLabel ? `${stepLabel} tints` : "tints";
604 | return [
605 | baseLine,
606 | "",
607 | shadesHeader,
608 | "-----",
609 | ...shadeLines,
610 | "",
611 | tintsHeader,
612 | "-----",
613 | ...tintLines
614 | ].join("\n");
615 | });
616 | return blocks.join("\n\n");
617 | };
618 |
619 | const getExportText = (state) => {
620 | let stepLabel = "";
621 | if (state && typeof state.tintShadeCount === "number" && state.tintShadeCount > 0) {
622 | const percentStep = Math.round(100 / state.tintShadeCount);
623 | stepLabel = `${percentStep}%`;
624 | }
625 | if (state.format === "css") return formatCssOutput(state.palettes);
626 | if (state.format === "json") return formatJsonOutput(state.palettes);
627 | if (state.format === "hex-hash") return formatHexOutput(state.palettes, true, stepLabel);
628 | if (state.format === "rgb") return formatRgbOutput(state.palettes, stepLabel);
629 | return formatHexOutput(state.palettes, false, stepLabel);
630 | };
631 |
632 | const updateExportOutput = (state, elements) => {
633 | if (!elements.output) return;
634 | const text = getExportText(state);
635 | if (elements.code) {
636 | elements.code.textContent = text;
637 | highlightExportCode(elements.code);
638 | } else {
639 | elements.output.textContent = text;
640 | }
641 | elements.output.setAttribute("aria-labelledby", `export-tab-${state.format}`);
642 | };
643 |
644 | const updateExportCornerRadius = (state, elements) => {
645 | if (!elements.output) return;
646 | const firstTabFormat = elements.tabs[0] ? elements.tabs[0].dataset.format : null;
647 | const isFirstTabActive = state.format === firstTabFormat;
648 | elements.output.classList.toggle("is-first-tab-active", isFirstTabActive);
649 | };
650 |
651 | const setExportFormat = (format, state, elements) => {
652 | state.format = VALID_EXPORT_FORMATS.includes(format) ? format : "hex";
653 | persistExportFormat(state.format);
654 | if (elements.tabs.length) {
655 | elements.tabs.forEach((tab) => {
656 | const isActive = tab.dataset.format === state.format;
657 | tab.classList.toggle("is-active", isActive);
658 | tab.setAttribute("aria-selected", isActive ? "true" : "false");
659 | tab.setAttribute("tabindex", isActive ? "0" : "-1");
660 | });
661 | }
662 | updateExportLanguage(state.format, elements);
663 | updateExportCornerRadius(state, elements);
664 | updateExportOutput(state, elements);
665 | resetExportScroll(elements);
666 | };
667 |
668 | const toggleExportWrapperVisibility = (visible, elements) => {
669 | if (!elements.wrapper || !elements.openButton) return;
670 | elements.wrapper.hidden = !visible;
671 | elements.openButton.disabled = !visible;
672 | elements.openButton.setAttribute("aria-expanded", "false");
673 | if (elements.imageButton) {
674 | elements.imageButton.disabled = !visible;
675 | elements.imageButton.setAttribute("aria-disabled", visible ? "false" : "true");
676 | }
677 | };
678 |
679 | const copyExportOutput = async (state, elements) => {
680 | const text = getExportText(state);
681 | if (!text) return;
682 | try {
683 | if (navigator.clipboard && navigator.clipboard.writeText) {
684 | await navigator.clipboard.writeText(text);
685 | } else {
686 | const helper = document.createElement("textarea");
687 | helper.value = text;
688 | helper.setAttribute("readonly", "");
689 | helper.style.position = "absolute";
690 | helper.style.left = "-9999px";
691 | document.body.appendChild(helper);
692 | helper.select();
693 | document.execCommand("copy");
694 | document.body.removeChild(helper);
695 | }
696 | const btn = elements.copyFab;
697 | if (btn) {
698 | btn.classList.add("copied");
699 | btn.disabled = true;
700 | btn.setAttribute("aria-disabled", "true");
701 | setTimeout(() => {
702 | btn.classList.remove("copied");
703 | btn.disabled = false;
704 | btn.setAttribute("aria-disabled", "false");
705 | }, 1500);
706 | }
707 | const status = elements.copyStatus;
708 | if (status) {
709 | status.textContent = "Copied export output to clipboard.";
710 | setTimeout(() => {
711 | status.textContent = "";
712 | }, 4500);
713 | }
714 | } catch (err) {
715 | console.error(err);
716 | }
717 | };
718 |
719 | const selectExportOutput = () => {
720 | if (!exportElements.output) return;
721 | const selection = window.getSelection && window.getSelection();
722 | if (!selection) return;
723 | const range = document.createRange();
724 | range.selectNodeContents(exportElements.output);
725 | selection.removeAllRanges();
726 | selection.addRange(range);
727 | };
728 |
729 | const openExportModal = (state, elements) => {
730 | if (!elements.modal) return;
731 | if (!state.palettes.length) return;
732 | setExportFormat(state.format, state, elements);
733 | lockBodyScroll();
734 | if (typeof elements.modal.showModal === "function") {
735 | elements.modal.showModal();
736 | } else {
737 | elements.modal.setAttribute("open", "true");
738 | }
739 | elements.modal.classList.remove("is-closing");
740 | elements.modal.removeAttribute("data-closing");
741 | if (!prefersReducedMotion()) {
742 | elements.modal.classList.add("is-opening");
743 | elements.modal.addEventListener("animationend", (event) => {
744 | if (event.target === elements.modal && event.animationName === "export-dialog-fade") {
745 | elements.modal.classList.remove("is-opening");
746 | }
747 | }, { once: true });
748 | } else {
749 | elements.modal.classList.remove("is-opening");
750 | }
751 | if (!handleOutsidePointerDown) {
752 | handleOutsidePointerDown = (event) => {
753 | if (!elements.modal || !elements.modal.open) return;
754 | const rect = elements.modal.getBoundingClientRect();
755 | const isOutside = event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
756 | if (isOutside) {
757 | closeExportModal(elements);
758 | }
759 | };
760 | document.addEventListener("pointerdown", handleOutsidePointerDown);
761 | }
762 | if (elements.openButton) {
763 | elements.openButton.setAttribute("aria-expanded", "true");
764 | }
765 | const activeTab = elements.tabs.find((tab) => tab.classList.contains("is-active")) || elements.tabs[0];
766 | if (activeTab) activeTab.focus();
767 | // Reset scroll after layout to avoid stale scroll positions on reopen
768 | requestAnimationFrame(() => resetExportScroll(elements));
769 | };
770 |
771 | const closeExportModal = (elements) => {
772 | const modal = elements.modal;
773 | if (!modal) return;
774 | if (modal.getAttribute("data-closing") === "true") return;
775 |
776 | const completeClose = () => {
777 | modal.removeAttribute("data-closing");
778 | modal.classList.remove("is-closing");
779 | modal.classList.remove("is-opening");
780 | if (modal.open && typeof modal.close === "function") {
781 | modal.close();
782 | } else {
783 | modal.removeAttribute("open");
784 | }
785 | resetExportScroll(elements);
786 | unlockBodyScroll();
787 | if (handleOutsidePointerDown) {
788 | document.removeEventListener("pointerdown", handleOutsidePointerDown);
789 | handleOutsidePointerDown = null;
790 | }
791 | if (elements.openButton) {
792 | elements.openButton.setAttribute("aria-expanded", "false");
793 | }
794 | };
795 |
796 | if (!modal.open && !modal.hasAttribute("open")) {
797 | completeClose();
798 | return;
799 | }
800 |
801 | if (prefersReducedMotion()) {
802 | completeClose();
803 | return;
804 | }
805 |
806 | modal.setAttribute("data-closing", "true");
807 | modal.classList.remove("is-opening");
808 | modal.classList.add("is-closing");
809 |
810 | let closeFallbackTimer = null;
811 | const handleAnimationEnd = (event) => {
812 | if (event.target !== modal || event.animationName !== "export-dialog-fade") return;
813 | clearTimeout(closeFallbackTimer);
814 | modal.removeEventListener("animationend", handleAnimationEnd);
815 | completeClose();
816 | };
817 |
818 | closeFallbackTimer = setTimeout(() => {
819 | modal.removeEventListener("animationend", handleAnimationEnd);
820 | completeClose();
821 | }, 250);
822 |
823 | modal.addEventListener("animationend", handleAnimationEnd);
824 | };
825 |
826 | const wireExportControls = () => {
827 | exportElements.wrapper = document.getElementById("palette-controls-wrapper");
828 | exportElements.openButton = document.getElementById("export-open");
829 | exportElements.modal = document.getElementById("export-dialog");
830 | exportElements.closeButton = document.getElementById("export-close");
831 | exportElements.tabs = Array.from(document.querySelectorAll(".export-tab"));
832 | exportElements.output = document.getElementById("export-output");
833 | exportElements.code = exportElements.output
834 | ? exportElements.output.querySelector(".export-output-code")
835 | : null;
836 | exportElements.copyFab = document.getElementById("export-copy");
837 | exportElements.copyStatus = document.getElementById("export-copy-status");
838 | exportElements.imageButton = document.getElementById("export-image");
839 |
840 | if (exportElements.openButton) {
841 | exportElements.openButton.addEventListener("click", () => openExportModal(exportState, exportElements));
842 | }
843 |
844 | if (exportElements.closeButton) {
845 | exportElements.closeButton.addEventListener("click", () => closeExportModal(exportElements));
846 | }
847 |
848 | if (exportElements.modal) {
849 | exportElements.modal.addEventListener("cancel", (event) => {
850 | event.preventDefault();
851 | closeExportModal(exportElements);
852 | });
853 | exportElements.modal.addEventListener("click", (event) => {
854 | // Prevent accidental close when dragging inside the dialog;
855 | // outside clicks are handled via document-level pointer listener.
856 | event.stopPropagation();
857 | });
858 | exportElements.modal.addEventListener("keydown", (event) => {
859 | if (event.key === "Escape") {
860 | event.preventDefault();
861 | closeExportModal(exportElements);
862 | return;
863 | }
864 | if (event.key === "Tab") {
865 | const focusables = getDialogFocusable();
866 | if (!focusables.length) return;
867 | const currentIndex = focusables.indexOf(document.activeElement);
868 | let nextIndex = currentIndex;
869 | if (event.shiftKey) {
870 | nextIndex = currentIndex <= 0 ? focusables.length - 1 : currentIndex - 1;
871 | } else {
872 | nextIndex = currentIndex === focusables.length - 1 ? 0 : currentIndex + 1;
873 | }
874 | focusables[nextIndex].focus();
875 | event.preventDefault();
876 | }
877 | });
878 |
879 | }
880 |
881 | if (exportElements.tabs.length) {
882 | exportElements.tabs.forEach((tab) => {
883 | tab.addEventListener("click", () => {
884 | setExportFormat(tab.dataset.format, exportState, exportElements);
885 | });
886 | tab.addEventListener("keydown", (event) => {
887 | if (event.key === "Enter" || event.key === " ") {
888 | event.preventDefault();
889 | tab.click();
890 | }
891 | if (event.key === "Home") {
892 | event.preventDefault();
893 | const firstTab = exportElements.tabs[0];
894 | if (firstTab) {
895 | firstTab.click();
896 | firstTab.focus();
897 | }
898 | }
899 | if (event.key === "End") {
900 | event.preventDefault();
901 | const lastTab = exportElements.tabs[exportElements.tabs.length - 1];
902 | if (lastTab) {
903 | lastTab.click();
904 | lastTab.focus();
905 | }
906 | }
907 | if (event.key === "ArrowRight" || event.key === "ArrowDown") {
908 | event.preventDefault();
909 | const currentIndex = exportElements.tabs.indexOf(tab);
910 | const nextIndex = (currentIndex + 1) % exportElements.tabs.length;
911 | exportElements.tabs[nextIndex].click();
912 | exportElements.tabs[nextIndex].focus();
913 | }
914 | if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
915 | event.preventDefault();
916 | const currentIndex = exportElements.tabs.indexOf(tab);
917 | const prevIndex = (currentIndex - 1 + exportElements.tabs.length) % exportElements.tabs.length;
918 | exportElements.tabs[prevIndex].click();
919 | exportElements.tabs[prevIndex].focus();
920 | }
921 | });
922 | });
923 | }
924 |
925 | if (exportElements.copyFab) {
926 | exportElements.copyFab.addEventListener("click", () => copyExportOutput(exportState, exportElements));
927 | }
928 |
929 | if (exportElements.output) {
930 | exportElements.output.addEventListener("click", (event) => {
931 | if (event.detail === 3) {
932 | event.preventDefault();
933 | selectExportOutput();
934 | }
935 | });
936 | }
937 |
938 | if (exportElements.imageButton) {
939 | exportElements.imageButton.addEventListener("click", () => downloadPaletteTableAsPng());
940 | }
941 |
942 | toggleExportWrapperVisibility(false, exportElements);
943 | setExportFormat(exportState.format, exportState, exportElements);
944 | };
945 |
946 | const updateClipboardData = (copyWithHashtag) => {
947 | const colorCells = document.querySelectorAll("#tints-and-shades td[data-clipboard-text]");
948 | colorCells.forEach(cell => {
949 | const colorCode = cell.getAttribute("data-clipboard-text");
950 | if (copyWithHashtag) {
951 | cell.setAttribute("data-clipboard-text", `#${colorCode}`);
952 | } else {
953 | cell.setAttribute("data-clipboard-text", colorCode.substr(1));
954 | }
955 | });
956 | };
957 |
958 | window.exportUI = {
959 | state: exportState,
960 | elements: exportElements,
961 | formatListOutput: formatHexOutput,
962 | formatCssOutput,
963 | formatJsonOutput,
964 | getExportText,
965 | updateExportOutput,
966 | setExportFormat,
967 | toggleExportWrapperVisibility,
968 | copyExportOutput,
969 | openExportModal,
970 | closeExportModal,
971 | wireExportControls,
972 | updateClipboardData
973 | };
974 | })();
975 |
--------------------------------------------------------------------------------