├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── makefile ├── preview.png ├── site ├── JetBrainsMono-Medium.woff2 └── index.html └── src ├── ColorMixer.elm ├── Helper ├── Color.elm ├── Icons.elm ├── Layout.elm └── Styles.elm ├── Main.elm ├── Model.elm ├── Section ├── File.elm ├── Mixer.elm └── Preview.elm └── Tests.elm /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | elm-stuff 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Hundred Rabbits theme editor 2 | Copyright Dzuk 2021 3 | 4 | NON-VIOLENT PUBLIC LICENSE v5 5 | 6 | Preamble 7 | 8 | The Non-Violent Public license is a freedom-respecting sharealike license 9 | for both the author of a work as well as those subject to a work. It aims 10 | to protect the basic rights of human beings from exploitation and the earth 11 | from plunder. It aims to ensure a copyrighted work is forever available 12 | for public use, modification, and redistribution under the same terms so 13 | long as the work is not used for harm. For more information about the NPL 14 | refer to the official webpage 15 | 16 | Official Webpage: https://thufie.lain.haus/NPL.html 17 | 18 | Terms and Conditions 19 | 20 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS 21 | NON-VIOLENT PUBLIC LICENSE v5 ("LICENSE"). THE WORK IS PROTECTED BY 22 | COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN 23 | AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY 24 | EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE 25 | TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE 26 | MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS 27 | CONTAINED HERE IN AS CONSIDERATION FOR ACCEPTING THE TERMS AND 28 | CONDITIONS OF THIS LICENSE AND FOR AGREEING TO BE BOUND BY THE TERMS 29 | AND CONDITIONS OF THIS LICENSE. 30 | 31 | 1. DEFINITIONS 32 | 33 | a. "Act of War" means any action of one country against any group 34 | either with an intention to provoke a conflict or an action that 35 | occurs during a declared war or during armed conflict between 36 | military forces of any origin. This includes but is not limited 37 | to enforcing sanctions or sieges, supplying armed forces, 38 | or profiting from the manufacture of tools or weaponry used in 39 | military conflict. 40 | 41 | b. "Adaptation" means a work based upon the Work, or upon the 42 | Work and other pre-existing works, such as a translation, 43 | adaptation, derivative work, arrangement of music or other 44 | alterations of a literary or artistic work, or phonogram or 45 | performance and includes cinematographic adaptations or any 46 | other form in which the Work may be recast, transformed, or 47 | adapted including in any form recognizably derived from the 48 | original, except that a work that constitutes a Collection will 49 | not be considered an Adaptation for the purpose of this License. 50 | For the avoidance of doubt, where the Work is a musical work, 51 | performance or phonogram, the synchronization of the Work in 52 | timed-relation with a moving image ("synching") will be 53 | considered an Adaptation for the purpose of this License. 54 | 55 | c. "Bodily Harm" means any physical hurt or injury to a person that 56 | interferes with the health or comfort of the person and that is more 57 | more than merely transient or trifling in nature. 58 | 59 | d. "Collection" means a collection of literary or artistic 60 | works, such as encyclopedias and anthologies, or performances, 61 | phonograms or broadcasts, or other works or subject matter other 62 | than works listed in Section 1(i) below, which, by reason of the 63 | selection and arrangement of their contents, constitute 64 | intellectual creations, in which the Work is included in its 65 | entirety in unmodified form along with one or more other 66 | contributions, each constituting separate and independent works 67 | in themselves, which together are assembled into a collective 68 | whole. A work that constitutes a Collection will not be 69 | considered an Adaptation (as defined above) for the purposes of 70 | this License. 71 | 72 | e. "Distribute" means to make available to the public the 73 | original and copies of the Work or Adaptation, as appropriate, 74 | through sale, gift or any other transfer of possession or 75 | ownership. 76 | 77 | f. "Incarceration" means confinement in a jail, prison, or any 78 | other place where individuals of any kind are held against 79 | either their will or the will of their legal guardians. 80 | 81 | g. "Licensor" means the individual, individuals, entity or 82 | entities that offer(s) the Work under the terms of this License. 83 | 84 | h. "Original Author" means, in the case of a literary or 85 | artistic work, the individual, individuals, entity or entities 86 | who created the Work or if no individual or entity can be 87 | identified, the publisher; and in addition (i) in the case of a 88 | performance the actors, singers, musicians, dancers, and other 89 | persons who act, sing, deliver, declaim, play in, interpret or 90 | otherwise perform literary or artistic works or expressions of 91 | folklore; (ii) in the case of a phonogram the producer being the 92 | person or legal entity who first fixes the sounds of a 93 | performance or other sounds; and, (iii) in the case of 94 | broadcasts, the organization that transmits the broadcast. 95 | 96 | i. "Work" means the literary and/or artistic work offered under 97 | the terms of this License including without limitation any 98 | production in the literary, scientific and artistic domain, 99 | whatever may be the mode or form of its expression including 100 | digital form, such as a book, pamphlet and other writing; a 101 | lecture, address, sermon or other work of the same nature; a 102 | dramatic or dramatico-musical work; a choreographic work or 103 | entertainment in dumb show; a musical composition with or 104 | without words; a cinematographic work to which are assimilated 105 | works expressed by a process analogous to cinematography; a work 106 | of drawing, painting, architecture, sculpture, engraving or 107 | lithography; a photographic work to which are assimilated works 108 | expressed by a process analogous to photography; a work of 109 | applied art; an illustration, map, plan, sketch or 110 | three-dimensional work relative to geography, topography, 111 | architecture or science; a performance; a broadcast; a 112 | phonogram; a compilation of data to the extent it is protected 113 | as a copyrightable work; or a work performed by a variety or 114 | circus performer to the extent it is not otherwise considered a 115 | literary or artistic work. 116 | 117 | j. "You" means an individual or entity exercising rights under 118 | this License who has not previously violated the terms of this 119 | License with respect to the Work, or who has received express 120 | permission from the Licensor to exercise rights under this 121 | License despite a previous violation. 122 | 123 | k. "Publicly Perform" means to perform public recitations of the 124 | Work and to communicate to the public those public recitations, 125 | by any means or process, including by wire or wireless means or 126 | public digital performances; to make available to the public 127 | Works in such a way that members of the public may access these 128 | Works from a place and at a place individually chosen by them; 129 | to perform the Work to the public by any means or process and 130 | the communication to the public of the performances of the Work, 131 | including by public digital performance; to broadcast and 132 | rebroadcast the Work by any means including signs, sounds or 133 | images. 134 | 135 | l. "Reproduce" means to make copies of the Work by any means 136 | including without limitation by sound or visual recordings and 137 | the right of fixation and reproducing fixations of the Work, 138 | including storage of a protected performance or phonogram in 139 | digital form or other electronic medium. 140 | 141 | m. "Software" means any digital Work which, through use of a 142 | third-party piece of Software or through the direct usage of 143 | itself on a computer system, the memory of the computer is 144 | modified dynamically or semi-dynamically. "Software", 145 | secondly, processes or interprets information. 146 | 147 | n. "Source Code" means the human-readable form of Software 148 | through which the Original Author and/or Distributor originally 149 | created, derived, and/or modified it. 150 | 151 | o. "Surveilling" means the use of the Work to either 152 | overtly or covertly observe and record persons and or their 153 | activities. 154 | 155 | p. "Network Service" means the use of a piece of Software to 156 | interpret or modify information that is subsequently and directly 157 | served to users over the Internet. 158 | 159 | q. "Discriminate" means the use of a work to differentiate between 160 | humans in a such a way which prioritizes some above others on the 161 | basis of percieved membership within certain groups. 162 | 163 | r. "Hate Speech" means communication or any form 164 | of expression which is solely for the purpose of expressing hatred 165 | for some group or advocating a form of Discrimination 166 | (to Discriminate per definition in (q)) between humans. 167 | 168 | s. "Coercion" means leveraging of the threat of force or use of force 169 | to intimidate a person in order to gain compliance, or to offer 170 | large incentives which aim to entice a person to act against their 171 | will. 172 | 173 | 2. FAIR DEALING RIGHTS 174 | 175 | Nothing in this License is intended to reduce, limit, or restrict any 176 | uses free from copyright or rights arising from limitations or 177 | exceptions that are provided for in connection with the copyright 178 | protection under copyright law or other applicable laws. 179 | 180 | 3. LICENSE GRANT 181 | 182 | Subject to the terms and conditions of this License, Licensor hereby 183 | grants You a worldwide, royalty-free, non-exclusive, perpetual (for the 184 | duration of the applicable copyright) license to exercise the rights in 185 | the Work as stated below: 186 | 187 | a. to Reproduce the Work, to incorporate the Work into one or 188 | more Collections, and to Reproduce the Work as incorporated in 189 | the Collections; 190 | 191 | b. to create and Reproduce Adaptations provided that any such 192 | Adaptation, including any translation in any medium, takes 193 | reasonable steps to clearly label, demarcate or otherwise 194 | identify that changes were made to the original Work. For 195 | example, a translation could be marked "The original work was 196 | translated from English to Spanish," or a modification could 197 | indicate "The original work has been modified."; 198 | 199 | c. to Distribute and Publicly Perform the Work including as 200 | incorporated in Collections; and, 201 | 202 | d. to Distribute and Publicly Perform Adaptations. The above 203 | rights may be exercised in all media and formats whether now 204 | known or hereafter devised. The above rights include the right 205 | to make such modifications as are technically necessary to 206 | exercise the rights in other media and formats. Subject to 207 | Section 8(g), all rights not expressly granted by Licensor are 208 | hereby reserved. 209 | 210 | 4. RESTRICTIONS 211 | 212 | The license granted in Section 3 above is expressly made subject to and 213 | limited by the following restrictions: 214 | 215 | a. You may Distribute or Publicly Perform the Work only under 216 | the terms of this License. You must include a copy of, or the 217 | Uniform Resource Identifier (URI) for, this License with every 218 | copy of the Work You Distribute or Publicly Perform. You may not 219 | offer or impose any terms on the Work that restrict the terms of 220 | this License or the ability of the recipient of the Work to 221 | exercise the rights granted to that recipient under the terms of 222 | the License. You may not sublicense the Work. You must keep 223 | intact all notices that refer to this License and to the 224 | disclaimer of warranties with every copy of the Work You 225 | Distribute or Publicly Perform. When You Distribute or Publicly 226 | Perform the Work, You may not impose any effective technological 227 | measures on the Work that restrict the ability of a recipient of 228 | the Work from You to exercise the rights granted to that 229 | recipient under the terms of the License. This Section 4(a) 230 | applies to the Work as incorporated in a Collection, but this 231 | does not require the Collection apart from the Work itself to be 232 | made subject to the terms of this License. If You create a 233 | Collection, upon notice from any Licensor You must, to the 234 | extent practicable, remove from the Collection any credit as 235 | required by Section 4(f), as requested. If You create an 236 | Adaptation, upon notice from any Licensor You must, to the 237 | extent practicable, remove from the Adaptation any credit as 238 | required by Section 4(f), as requested. 239 | 240 | b. If the Work meets the definition of Software, You may exercise 241 | the rights granted in Section 3 only if You provide a copy of the 242 | corresponding Source Code from which the Work was derived in digital 243 | form, or You provide a URI for the corresponding Source Code of 244 | the Work, to any recipients upon request. 245 | 246 | c. If the Work is used as or for a Network Service, You may exercise 247 | the rights granted in Section 3 only if You provide a copy of the 248 | corresponding Source Code from which the Work was derived in digital 249 | form, or You provide a URI for the corresponding Source Code to the 250 | Work, to any recipients of the data served or modified by the Web 251 | Service. 252 | 253 | d. You may exercise the rights granted in Section 3 for 254 | any purposes only if: 255 | 256 | i. You do not use the Work for the purpose of inflicting 257 | Bodily Harm on human beings (subject to criminal 258 | prosecution or otherwise) outside of providing medical aid 259 | or undergoing a voluntary procedure under no form of 260 | Coercion. 261 | ii.You do not use the Work for the purpose of Surveilling 262 | or tracking individuals for financial gain. 263 | iii. You do not use the Work in an Act of War. 264 | iv. You do not use the Work for the purpose of supporting 265 | or profiting from an Act of War. 266 | v. You do not use the Work for the purpose of Incarceration. 267 | vi. You do not use the Work for the purpose of extracting, 268 | processing, or refining, oil, gas, or coal. Or to in any other 269 | way to deliberately pollute the environment as a byproduct 270 | of manufacturing or irresponsible disposal of hazardous materials. 271 | vii. You do not use the Work for the purpose of 272 | expediting, coordinating, or facilitating paid work 273 | undertaken by individuals under the age of 12 years. 274 | viii. You do not use the Work to either Discriminate or 275 | spread Hate Speech on the basis of sex, sexual orientation, 276 | gender identity, race, age, disability, color, national origin, 277 | religion, or lower economic status. 278 | 279 | e. If You Distribute, or Publicly Perform the Work or any 280 | Adaptations or Collections, You must, unless a request has been 281 | made pursuant to Section 4(a), keep intact all copyright notices 282 | for the Work and provide, reasonable to the medium or means You 283 | are utilizing: (i) the name of the Original Author (or 284 | pseudonym, if applicable) if supplied, and/or if the Original 285 | Author and/or Licensor designate another party or parties (e.g., 286 | a sponsor institute, publishing entity, journal) for attribution 287 | ("Attribution Parties") in Licensor's copyright notice, terms of 288 | service or by other reasonable means, the name of such party or 289 | parties; (ii) the title of the Work if supplied; (iii) to the 290 | extent reasonably practicable, the URI, if any, that Licensor 291 | specifies to be associated with the Work, unless such URI does 292 | not refer to the copyright notice or licensing information for 293 | the Work; and, (iv) consistent with Section 3(b), in the case of 294 | an Adaptation, a credit identifying the use of the Work in the 295 | Adaptation (e.g., "French translation of the Work by Original 296 | Author," or "Screenplay based on original Work by Original 297 | Author"). The credit required by this Section 4(e) may be 298 | implemented in any reasonable manner; provided, however, that in 299 | the case of an Adaptation or Collection, at a minimum such credit 300 | will appear, if a credit for all contributing authors of the 301 | Adaptation or Collection appears, then as part of these credits 302 | and in a manner at least as prominent as the credits for the 303 | other contributing authors. For the avoidance of doubt, You may 304 | only use the credit required by this Section for the purpose of 305 | attribution in the manner set out above and, by exercising Your 306 | rights under this License, You may not implicitly or explicitly 307 | assert or imply any connection with, sponsorship or endorsement 308 | by the Original Author, Licensor and/or Attribution Parties, as 309 | appropriate, of You or Your use of the Work, without the 310 | separate, express prior written permission of the Original 311 | Author, Licensor and/or Attribution Parties. 312 | 313 | f. Except as otherwise agreed in writing by the Licensor or as 314 | may be otherwise permitted by applicable law, if You Reproduce, 315 | Distribute or Publicly Perform the Work either by itself or as 316 | part of any Adaptations or Collections, You must not distort, 317 | mutilate, modify or take other derogatory action in relation to 318 | the Work which would be prejudicial to the Original Author's 319 | honor or reputation. Licensor agrees that in those jurisdictions 320 | (e.g. Japan), in which any exercise of the right granted in 321 | Section 3(b) of this License (the right to make Adaptations) 322 | would be deemed to be a distortion, mutilation, modification or 323 | other derogatory action prejudicial to the Original Author's 324 | honor and reputation, the Licensor will waive or not assert, as 325 | appropriate, this Section, to the fullest extent permitted by 326 | the applicable national law, to enable You to reasonably 327 | exercise Your right under Section 3(b) of this License (right to 328 | make Adaptations) but not otherwise. 329 | 330 | g. Do not make any legal claim against anyone accusing the 331 | Work, with or without changes, alone or with other works, 332 | of infringing any patent claim. 333 | 334 | 5. REPRESENTATIONS, WARRANTIES AND DISCLAIMER 335 | 336 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR 337 | OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY 338 | KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, 339 | INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, 340 | FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF 341 | LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF 342 | ERRORS, WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW 343 | THE EXCLUSION OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO 344 | YOU. 345 | 346 | 6. LIMITATION ON LIABILITY 347 | 348 | EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL 349 | LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, 350 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF 351 | THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS BEEN ADVISED 352 | OF THE POSSIBILITY OF SUCH DAMAGES. 353 | 354 | 7. TERMINATION 355 | 356 | a. This License and the rights granted hereunder will terminate 357 | automatically upon any breach by You of the terms of this 358 | License. Individuals or entities who have received Adaptations 359 | or Collections from You under this License, however, will not 360 | have their licenses terminated provided such individuals or 361 | entities remain in full compliance with those licenses. Sections 362 | 1, 2, 5, 6, 7, and 8 will survive any termination of this 363 | License. 364 | 365 | b. Subject to the above terms and conditions, the license 366 | granted here is perpetual (for the duration of the applicable 367 | copyright in the Work). Notwithstanding the above, Licensor 368 | reserves the right to release the Work under different license 369 | terms or to stop distributing the Work at any time; provided, 370 | however that any such election will not serve to withdraw this 371 | License (or any other license that has been, or is required to 372 | be, granted under the terms of this License), and this License 373 | will continue in full force and effect unless terminated as 374 | stated above. 375 | 376 | 8. REVISED LICENSE VERSIONS 377 | 378 | a. This License may receive future revisions in the original 379 | spirit of the license intended to strengthen This License. 380 | Each version of This License has an incrementing version number. 381 | 382 | b. Unless otherwise specified like in Section 8(c) The Licensor 383 | has only granted this current version of This License for The Work. 384 | In this case future revisions do not apply. 385 | 386 | c. The Licensor may specify that the latest available 387 | revision of This License be used for The Work by either explicitly 388 | writing so or by suffixing the License URI with a "+" symbol. 389 | 390 | d. The Licensor may specify that The Work is also available 391 | under the terms of This License's current revision as well 392 | as specific future revisions. The Licensor may do this by 393 | writing it explicitly or suffixing the License URI with any 394 | additional version numbers each separated by a comma. 395 | 396 | 9. MISCELLANEOUS 397 | 398 | a. Each time You Distribute or Publicly Perform the Work or a 399 | Collection, the Licensor offers to the recipient a license to 400 | the Work on the same terms and conditions as the license granted 401 | to You under this License. 402 | 403 | b. Each time You Distribute or Publicly Perform an Adaptation, 404 | Licensor offers to the recipient a license to the original Work 405 | on the same terms and conditions as the license granted to You 406 | under this License. 407 | 408 | c. If the Work is classified as Software, each time You Distribute 409 | or Publicly Perform an Adaptation, Licensor offers to the recipient 410 | a copy and/or URI of the corresponding Source Code on the same 411 | terms and conditions as the license granted to You under this License. 412 | 413 | d. If the Work is used as a Network Service, each time You Distribute 414 | or Publicly Perform an Adaptation, or serve data derived from the 415 | Software, the Licensor offers to any recipients of the data a copy 416 | and/or URI of the corresponding Source Code on the same terms and 417 | conditions as the license granted to You under this License. 418 | 419 | e. If any provision of this License is invalid or unenforceable 420 | under applicable law, it shall not affect the validity or 421 | enforceability of the remainder of the terms of this License, 422 | and without further action by the parties to this agreement, 423 | such provision shall be reformed to the minimum extent necessary 424 | to make such provision valid and enforceable. 425 | 426 | f. No term or provision of this License shall be deemed waived 427 | and no breach consented to unless such waiver or consent shall 428 | be in writing and signed by the party to be charged with such 429 | waiver or consent. 430 | 431 | g. This License constitutes the entire agreement between the 432 | parties with respect to the Work licensed here. There are no 433 | understandings, agreements or representations with respect to 434 | the Work not specified here. Licensor shall not be bound by any 435 | additional provisions that may appear in any communication from 436 | You. This License may not be modified without the mutual written 437 | agreement of the Licensor and You. 438 | 439 | h. The rights granted under, and the subject matter referenced, 440 | in this License were drafted utilizing the terminology of the 441 | Berne Convention for the Protection of Literary and Artistic 442 | Works (as amended on September 28, 1979), the Rome Convention of 443 | 1961, the WIPO Copyright Treaty of 1996, the WIPO Performances 444 | and Phonograms Treaty of 1996 and the Universal Copyright 445 | Convention (as revised on July 24, 1971). These rights and 446 | subject matter take effect in the relevant jurisdiction in which 447 | the License terms are sought to be enforced according to the 448 | corresponding provisions of the implementation of those treaty 449 | provisions in the applicable national law. If the standard suite 450 | of rights granted under applicable copyright law includes 451 | additional rights not granted under this License, such 452 | additional rights are deemed to be included in the License; this 453 | License is not intended to restrict the license of any rights 454 | under applicable law. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hundred Rabbits theme editor 2 | 3 | ![](preview.png) 4 | 5 | Create and edit [Hundred Rabbits themes](https://github.com/hundredrabbits/Themes) using this simple web app. 6 | 7 | [**Click here to use the editor.**](https://dzuk-mutant.github.io/100r-theme-editor/) 8 | 9 | --- 10 | 11 | # Instructions 12 | 13 | - Drag and drop themes into the window (or click the import button) 14 | to view and edit them. 15 | - Click on the different color names in the middle section to select them for editing. 16 | - Use the color controls at the bottom section to adjust the selected color. 17 | - Click 'HSL' for HSL sliders and 'RGB' for RGB sliders. 18 | - You can use sliders or the text boxes next to them to edit color values. 19 | - You can enter hex values in the hex text box. (It will revert to the previous color if there was an error in the hex colour that you give.) 20 | - All changes are immediate, and you can see the tests and accessibility score of your theme in real time. 21 | - Click the download button to download your new theme. 22 | 23 | 24 | ## Testing 25 | 26 | There are two components to testing a Hundred Rabbits theme: 27 | 28 | ### Basic Tests 29 | 30 | The way colours work in Hundred Rabbits themes is that the high, medium and low colours should be contrasted against the background in order - `f_high` be more contrasting against the background than `f_med`, and so on. 31 | 32 | The basic tests at the top will tell you if the contrast should be swapped (and arrows will appear on the colour buttons indicating as well). If they're all good, it will say 'passed!'. 33 | 34 | ### Contrast 35 | 36 | Each colour combination in the preview grid has a number and a grade attached. The number is a score showing how contrasted the colour combination is, and the grade tells you what WCAG guidelines it passes. 37 | 38 | - 3 and under is 'X', which here means that it didn't pass any guidelines. 39 | - 3 - 4.5 is 'A'. (recommended min. for people with regular vision) 40 | - 4.5 - 7 is 'AA'. (recommended min. for people with 40/20 vision) 41 | - 7 and above is 'AAA'. (recommended min. for people with 80/20 vision) 42 | 43 | At the top of the theme, you'll see the overall 'theme contrast', this tells you the minimum contrast in your colour combinations. 44 | 45 | There are no real wrong answers with contrast accessibility when it comes to making themes for yourself - some people absolutely need things that are contrasted enough, but some people much prefer lower contrast. These scores are just a tool to help you make informed design choices. 46 | 47 | --- 48 | 49 | # Accessibility 50 | 51 | - This has not been built with screenreaders in mind, I will look into it in the future if people ask for it. 52 | - All measurements are in rems, so it will scale with text size. 53 | - Internet is required for building but works offline once built. 54 | 55 | --- 56 | 57 | # Building 58 | 59 | Building requires the following: 60 | 61 | - Elm 0.19.1 (can be installed via `npm install elm`) 62 | - terser (can be installed via `npm install terser`) 63 | 64 | To build all of it, run `make all`. 65 | 66 | An internet connection is required for the initial build, but 67 | the app will work offline once built. 68 | 69 | --- 70 | 71 | # Licenses and acknowledgments 72 | 73 | - This software is licensed [NPL v5](LICENSE). 74 | - The license is marked differently in `elm.json` because currently the Elm compiler will not compile if there isn't an OSI-approved license there. 75 | - JetBrains Mono is licensed OFL 1.1. 76 | - The default theme for the app is Hundred Rabbits' [noir theme](https://github.com/hundredrabbits/Themes/blob/master/themes/noir.svg). 77 | 78 | 79 | --- 80 | 81 | # Contributions 82 | 83 | I'm open to bug requests, but other than that, this is a small personal project and I'd rather keep it that way right now. 84 | 85 | The issues section is there largely for my own personal tracking and to make development easier to understand to outsiders. -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "avh4/elm-color": "1.0.0", 10 | "dzuk-mutant/elm-responsive-pixels": "1.2.0", 11 | "dzuk-mutant/hundred-rabbits-themes-elm": "1.2.0", 12 | "elm/browser": "1.0.2", 13 | "elm/core": "1.0.5", 14 | "elm/file": "1.0.5", 15 | "elm/html": "1.0.0", 16 | "elm/json": "1.1.3", 17 | "noahzgordon/elm-color-extra": "1.0.2", 18 | "rtfeldman/elm-css": "16.1.0", 19 | "ymtszw/elm-xml-decode": "3.2.1" 20 | }, 21 | "indirect": { 22 | "elm/bytes": "1.0.8", 23 | "elm/parser": "1.1.0", 24 | "elm/regex": "1.0.0", 25 | "elm/svg": "1.0.1", 26 | "elm/time": "1.0.0", 27 | "elm/url": "1.0.0", 28 | "elm/virtual-dom": "1.0.2", 29 | "fredcy/elm-parseint": "2.0.1", 30 | "jinjor/elm-xml-parser": "2.0.0", 31 | "rtfeldman/elm-hex": "1.0.0" 32 | } 33 | }, 34 | "test-dependencies": { 35 | "direct": {}, 36 | "indirect": {} 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | client_elm_out=./bin/main.js 2 | elm_compress='pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' 3 | assets_out=./bin 4 | 5 | ghi_elm_out=./main.js 6 | 7 | all: 8 | make elm 9 | make assets 10 | 11 | github-io: 12 | make elm-ghi 13 | cp -R ./site/. ./ 14 | 15 | elm: 16 | elm make ./src/Main.elm --optimize --output=$(client_elm_out) 17 | terser $(client_elm_out) --compress $(elm_compress) | terser --mangle --output $(client_elm_out) 18 | 19 | elm-ghi: 20 | elm make ./src/Main.elm --optimize --output=$(ghi_elm_out) 21 | terser $(ghi_elm_out) --compress $(elm_compress) | terser --mangle --output $(ghi_elm_out) 22 | 23 | debug: 24 | elm make ./src/Main.elm --output=$(client_elm_out) 25 | 26 | assets: 27 | cp -R ./site/. $(assets_out) 28 | 29 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzuk-mutant/100r-theme-editor/75a3858f6bba8798eae7a9db02c40a16a1f242e7/preview.png -------------------------------------------------------------------------------- /site/JetBrainsMono-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dzuk-mutant/100r-theme-editor/75a3858f6bba8798eae7a9db02c40a16a1f242e7/site/JetBrainsMono-Medium.woff2 -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hundred Rabbits Theme Test 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /src/ColorMixer.elm: -------------------------------------------------------------------------------- 1 | module ColorMixer exposing ( ColorMixer 2 | , EditActivity(..) 3 | , RGBEdit(..) 4 | , HSLEdit(..) 5 | 6 | , fromColor 7 | , edit 8 | 9 | , intToVal 10 | , stringToVal 11 | , valToInt 12 | , valToString 13 | ) 14 | 15 | 16 | {-| A module for editing a colour in a user interface. 17 | 18 | # Types 19 | @docs ColorMixer, EditActivity, RGBEdit, HSLEdit 20 | 21 | # Create 22 | @docs fromColor 23 | 24 | # Edit 25 | @docs edit 26 | 27 | ## Convert values 28 | 29 | While ColorMixer stores Colors as Floats for 30 | high resolution and inter-operability with `Color`, 31 | it's likely you'll want to be converting to and from 32 | other types of inputs. 33 | 34 | @docs intToVal, stringToVal, valToInt, valToString 35 | 36 | -} 37 | 38 | import Color exposing (Color) 39 | import Color.Convert exposing (colorToHex) 40 | 41 | 42 | {-| A type representing a color mixer in a user interface. 43 | 44 | - `color` : The current color being worked on. 45 | - `red` : The value for the red slider in RGB controls. 46 | - `green` : The value for the green slider in RGB controls. 47 | - `blue` : The value for the blue slider in RGB controls. 48 | - `hue` : The value for the hue slider in HSL controls. 49 | - `saturation` : The value for the saturation slider in HSL controls. 50 | - `lightness` : The value for the lightness slider in HSL controls. 51 | - `hex` : The value for the hex text input. 52 | -} 53 | type alias ColorMixer = 54 | { color : Color 55 | 56 | , red : Float 57 | , green : Float 58 | , blue : Float 59 | 60 | , hue : Float 61 | , saturation : Float 62 | , lightness : Float 63 | 64 | , hex : String 65 | } 66 | 67 | 68 | {-| The range of possibilities for controlling/editing a 69 | ColorMixer. 70 | 71 | - `ColorChange` - when the color this mixer operates 72 | is changed altogether. 73 | - `RGBEdited` - when RGB sliders/values are edited. 74 | - `HSLEdited` - when HSL sliders/values are edited. 75 | 76 | Hex works differently because the mixer's color can't directly 77 | be saved as the user is typing, only until they focus away from 78 | the text input. 79 | 80 | ### HexEdited 81 | When a Hex value representing this colour 82 | is being edited right now. 83 | 84 | ### HexDone 85 | Setting a hex's input is different to editing 86 | based on hex input because we can't be sure 87 | if the user is correct or not until they're 88 | finished typing. 89 | 90 | This function means that the user has done editing 91 | the hex input and we can safely apply the user's 92 | hex value to everything including the hex input itself. 93 | 94 | This means that if the user's input is wrong, it will 95 | revert to the last good known hex input. 96 | 97 | -} 98 | type EditActivity 99 | = ColorChanged Color 100 | | RGBEdited RGBEdit 101 | | HSLEdited HSLEdit 102 | | HexEdited String 103 | | HexDone 104 | 105 | type RGBEdit 106 | = Red Float 107 | | Green Float 108 | | Blue Float 109 | 110 | type HSLEdit 111 | = Hue Float 112 | | Saturation Float 113 | | Lightness Float 114 | 115 | 116 | {-| Creates a new mixer from a Color value. 117 | -} 118 | fromColor : Color -> ColorMixer 119 | fromColor newColor = 120 | let 121 | newRgb = Color.toRgba newColor 122 | newHsl = Color.toHsla newColor 123 | in 124 | { color = newColor 125 | 126 | , red = newRgb.red 127 | , green = newRgb.green 128 | , blue = newRgb.blue 129 | 130 | , hue = newHsl.hue 131 | , saturation = newHsl.saturation 132 | , lightness = newHsl.lightness 133 | 134 | , hex = colorToHex newColor 135 | } 136 | 137 | 138 | {-| Takes an EditActivity and a ColorMixer 139 | and edits the ColorMixer based on that activity. 140 | -} 141 | edit : EditActivity -> ColorMixer -> ColorMixer 142 | edit activity mixer = 143 | case activity of 144 | ColorChanged c -> fromColor c -- we can just reuse fromColor. 145 | RGBEdited r -> rgbEdit r mixer 146 | HSLEdited h -> hslEdit h mixer 147 | HexEdited x -> hexEdit x mixer 148 | HexDone -> hexSet mixer 149 | 150 | 151 | {-| Takes an String representing a Int-based color 152 | value with a min anx max bounds and turns it into a 153 | Float that's between 0 and 1 so it can be updated 154 | to a ColorMixer. 155 | 156 | onInput <| RGBEdit << Blue << (ColorMixer.stringToVal 0 255) 157 | 158 | If the input number is higher than the max bounds 159 | you've given, then this function will clamp it. 160 | 161 | If the number conversion fails, the value will be 0. 162 | -} 163 | stringToVal : Int -> Int -> String -> Float 164 | stringToVal minVal maxVal str = 165 | str 166 | |> String.toInt 167 | -- this should be used in an environment 168 | -- where failure is not possible 169 | |> Maybe.withDefault 0 170 | |> intToVal minVal maxVal 171 | 172 | {-| Takes an int representing a color value with 173 | a min anx max bounds and turns it into a Float 174 | that's between 0 and 1 so it can be updated to a 175 | ColorMixer. 176 | 177 | HSLEdit <| Hue <| stringToVal 0 360 hue 178 | 179 | If the number given is higher than the max bounds 180 | you've given, then this function will clamp it. 181 | -} 182 | intToVal : Int -> Int -> Int -> Float 183 | intToVal minVal maxVal int = 184 | int 185 | {- it's important that they are clamped because the value 186 | could be coming from a slider or a number box, in the latter, 187 | the user can still enter an invalidly high/low value even with 188 | min/max attrs. 189 | 190 | So clamping makes sure they can never do that. 191 | -} 192 | |> clamp minVal maxVal 193 | |> toFloat 194 | |> (\c -> c / toFloat maxVal) 195 | 196 | 197 | {-| Takes a value from the mixer and converts it to an Int 198 | at the scale provided by the max value (the first Int.) 199 | 200 | ColorMixer.valToInt 255 model.mixer.green 201 | -} 202 | valToInt : Int -> Float -> Int 203 | valToInt maxVal val = 204 | val 205 | |> (*) (toFloat maxVal) 206 | |> round 207 | 208 | {-| Takes a value from the mixer and converts it to an String 209 | representing an int at the scale provided by the max value 210 | (the first Int.) 211 | 212 | value <| ColorMixer.valToString 255 model.mixer.green 213 | -} 214 | valToString : Int -> Float -> String 215 | valToString maxVal val = 216 | val 217 | |> valToInt maxVal 218 | |> String.fromInt 219 | 220 | 221 | ------------------------------------------------------- 222 | ------------------------------------------------------- 223 | ------------------------------------------------------- 224 | --------------------- INTERNAL ------------------------ 225 | ------------------------------------------------------- 226 | ------------------------------------------------------- 227 | ------------------------------------------------------- 228 | 229 | 230 | {-| Updates a ColorMixer based on an RGBEdit action. 231 | -} 232 | rgbEdit : RGBEdit -> ColorMixer -> ColorMixer 233 | rgbEdit rgbAction mixer = 234 | let 235 | currentRgb = Color.toRgba mixer.color 236 | newColor = 237 | case rgbAction of 238 | Red newRed -> Color.rgb (clampZeroOne newRed) currentRgb.green currentRgb.blue 239 | Green newGreen -> Color.rgb currentRgb.red (clampZeroOne newGreen) currentRgb.blue 240 | Blue newBlue -> Color.rgb currentRgb.red currentRgb.green (clampZeroOne newBlue) 241 | in 242 | -- Apply the newly mixed color to everything. 243 | fromColor newColor 244 | 245 | 246 | 247 | 248 | 249 | {-| Updates a ColorMixer based on an HSLEdit action. 250 | 251 | HSL acts differently to RGB so it needs its own 252 | updating function. 253 | -} 254 | hslEdit : HSLEdit -> ColorMixer -> ColorMixer 255 | hslEdit hslAction mixer = 256 | let 257 | newColor = 258 | -- HSL operations have to look to the sliders 259 | -- rather than the conversions from the color. 260 | case hslAction of 261 | Hue newHue -> 262 | Color.hsl (clampZeroOne newHue) mixer.saturation mixer.lightness 263 | 264 | Saturation newSaturation -> 265 | Color.hsl mixer.hue (clampZeroOne newSaturation) mixer.lightness 266 | 267 | Lightness newLightness -> 268 | Color.hsl mixer.hue mixer.saturation (clampZeroOne newLightness) 269 | 270 | -- Only update the slider the user is changing. 271 | updateHSlSliders = 272 | case hslAction of 273 | Hue newHue -> 274 | (\m -> { m | hue = newHue }) 275 | 276 | Saturation newSaturation -> 277 | (\m -> { m | saturation = newSaturation }) 278 | 279 | Lightness newLightness -> 280 | (\m -> { m | lightness = newLightness }) 281 | 282 | newRgb = Color.toRgba newColor 283 | 284 | in 285 | -- Apply the newly mixed color to everything but HSL sliders. 286 | { mixer | color = newColor 287 | 288 | , red = newRgb.red 289 | , green = newRgb.green 290 | , blue = newRgb.blue 291 | 292 | , hex = colorToHex newColor 293 | } 294 | -- Only change the HSL slider that the user changed. 295 | |> updateHSlSliders 296 | 297 | 298 | 299 | {-| This tries to put the hex color into `color` as 300 | the user is typing in. So this leaves the hex alone 301 | while it tries to edit the colors around it. 302 | -} 303 | hexEdit : String -> ColorMixer -> ColorMixer 304 | hexEdit newHex mixer = 305 | let 306 | newColor = tryToEditHex newHex mixer.color 307 | newRgb = Color.toRgba newColor 308 | newHsl = Color.toHsla newColor 309 | in 310 | 311 | { mixer | color = newColor 312 | 313 | , red = newRgb.red 314 | , green = newRgb.green 315 | , blue = newRgb.blue 316 | 317 | , hue = newHsl.hue 318 | , saturation = newHsl.saturation 319 | , lightness = newHsl.lightness 320 | 321 | -- edit the hex based on what 322 | -- the user is typing right now. 323 | , hex = newHex 324 | } 325 | 326 | 327 | {-| Setting a hex's input is different to editing 328 | based on hex input because we can't be sure 329 | if the user is correct or not until they're 330 | finished typing. 331 | 332 | This function means that the user has done editing 333 | the hex input and we can safely apply the user's 334 | hex value to everything including the hex input itself. 335 | 336 | This means that if the user's input is wrong, it will 337 | revert to the last good known hex input. 338 | -} 339 | hexSet : ColorMixer -> ColorMixer 340 | hexSet mixer = 341 | let 342 | newColor = tryToEditHex mixer.hex mixer.color 343 | in 344 | fromColor newColor 345 | 346 | 347 | 348 | 349 | ------------------------------------------------------- 350 | ------------------------------------------------------- 351 | ------------------------------------------------------- 352 | ------------ INTERNAL OF THE INTERNAL ----------------- 353 | ------------------------------------------------------- 354 | ------------------------------------------------------- 355 | ------------------------------------------------------- 356 | 357 | 358 | 359 | {-| Internal function that tries to create a new 360 | Color based on hex input. If the hex input fails, 361 | it returns the existing color. 362 | -} 363 | tryToEditHex : String -> Color -> Color 364 | tryToEditHex newHexStr currentColor = 365 | let 366 | color = currentColor 367 | 368 | hashed = 369 | if String.left 1 newHexStr == "#" then 370 | newHexStr 371 | else 372 | "#" ++ newHexStr 373 | 374 | -- if the new color isn't good, keep the old one 375 | newColor = hashed 376 | |> Color.Convert.hexToColor 377 | |> Result.toMaybe 378 | |> Maybe.withDefault color 379 | in 380 | newColor 381 | 382 | 383 | {-| Ensures that floats coming in are properly clamped 384 | between 0 and 1 because that's what Color requires. 385 | -} 386 | clampZeroOne : Float -> Float 387 | clampZeroOne = clamp 0 1 -------------------------------------------------------------------------------- /src/Helper/Color.elm: -------------------------------------------------------------------------------- 1 | module Helper.Color exposing ( convColor 2 | 3 | , getNewSelectedColor 4 | , changeSelectedColor 5 | ) 6 | 7 | import Color 8 | import Color.Convert 9 | import Css 10 | import HRTheme exposing (HRTheme) 11 | import Model exposing (Model, SelectedColor(..)) 12 | 13 | {-| Converts avh4's elm-color 14 | to elm-css's Color type. 15 | -} 16 | convColor : Color.Color -> Css.Color 17 | convColor colorData = 18 | colorData 19 | |> Color.Convert.colorToHex 20 | |> Css.hex 21 | 22 | 23 | 24 | {-| Returns a color from a newly selected colour 25 | (ie. one that would be in an update function case) 26 | -} 27 | getNewSelectedColor : SelectedColor -> HRTheme -> Color.Color 28 | getNewSelectedColor selCol theme = 29 | case selCol of 30 | Background -> theme.background 31 | FHigh -> theme.fHigh 32 | FMed -> theme.fMed 33 | FLow -> theme.fLow 34 | FInv -> theme.fInv 35 | BHigh -> theme.bHigh 36 | BMed -> theme.bMed 37 | BLow -> theme.bLow 38 | BInv -> theme.bInv 39 | 40 | {-| Takes a colour and applies it to the current 41 | selected colour in the theme and returns the new theme. 42 | -} 43 | changeSelectedColor : Color.Color -> Model -> HRTheme 44 | changeSelectedColor color model = 45 | let 46 | c = color 47 | t = model.theme 48 | in 49 | case model.selectedColor of 50 | Background -> { t | background = c } 51 | FHigh -> { t | fHigh = c } 52 | FMed -> { t | fMed = c } 53 | FLow -> { t | fLow = c } 54 | FInv -> { t | fInv = c } 55 | BHigh -> { t | bHigh = c } 56 | BMed -> { t | bMed = c } 57 | BLow -> { t | bLow = c } 58 | BInv -> { t | bInv = c } 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/Helper/Icons.elm: -------------------------------------------------------------------------------- 1 | module Helper.Icons exposing (importIcon, download) 2 | 3 | import Css exposing (displayFlex, property) 4 | import Svg.Styled exposing (Svg, g, path, svg) 5 | import Svg.Styled.Attributes exposing (css, d, version, viewBox ) 6 | 7 | 8 | icon : String -> List ( Svg msg ) -> Svg msg 9 | icon viewBoxSize iconGeometry = 10 | svg [ viewBox viewBoxSize 11 | , version "1.1" 12 | , css [ property "fill-rule" "evenodd" -- prevents punched holes from disappearing. 13 | , property "fill" "currentColor" -- allows icons to inherit colours specified further up the chain. 14 | , property "flex" "0 0 auto" -- keeps the sizing stable and static. 15 | , displayFlex 16 | ] 17 | ] 18 | [ g [] 19 | iconGeometry 20 | ] 21 | 22 | importIcon : Svg msg 23 | importIcon = 24 | icon 25 | "0 0 36 28" 26 | [ path [ d "M16.5,17.621l-4.439,4.44l-2.122,-2.122l7,-7c0.586,-0.585 1.536,-0.585 2.122,0l7,7l-2.122,2.122l-4.439,-4.44l-0,9.379l-3,0l-0,-9.379Zm-12,5.879l5.5,-0l-0,3l-7,-0c-0.828,0 -1.5,-0.672 -1.5,-1.5l-0,-22c-0,-0.828 0.672,-1.5 1.5,-1.5l30,-0c0.828,0 1.5,0.672 1.5,1.5l-0,22c-0,0.828 -0.672,1.5 -1.5,1.5l-6,-0l-0,-3l4.5,-0l-0,-19l-27,-0l-0,19Z" ][]] 27 | 28 | download : Svg msg 29 | download = 30 | icon 31 | "0 0 36 28" 32 | [ path [ d "M31.5,23.5l-0,-4.5l3,0l-0,6c-0,0.828 -0.672,1.5 -1.5,1.5l-30,0c-0.828,0 -1.5,-0.672 -1.5,-1.5l-0,-6l3,0l-0,4.5l27,0Zm-15,-8.121l-0,-9.379l3,-0l-0,9.379l4.439,-4.44l2.122,2.122l-7,7c-0.586,0.585 -1.536,0.585 -2.122,-0l-7,-7l2.122,-2.122l4.439,4.44Z" ][]] 33 | -------------------------------------------------------------------------------- /src/Helper/Layout.elm: -------------------------------------------------------------------------------- 1 | module Helper.Layout exposing (..) 2 | 3 | import Css exposing (..) 4 | import Helper.Color exposing (convColor) 5 | import Helper.Styles exposing (cellWidth) 6 | import HRTheme exposing (HRTheme) 7 | import Html.Styled as Html exposing (Attribute, Html, button) 8 | import Html.Styled.Attributes exposing (css) 9 | import Rpx exposing (blc) 10 | 11 | 12 | cellButton : HRTheme -> Bool -> List (Attribute msg) -> List Style -> String -> Html msg 13 | cellButton theme ifSelected attrs styles label = 14 | button 15 | ( [ css 16 | ( [ Helper.Styles.buttonStyles 17 | 18 | , minWidth <| Rpx.add cellWidth (blc 2) 19 | , height (blc 5) 20 | , padding (blc 1) 21 | 22 | , border zero 23 | , fontWeight (int 600) 24 | , Helper.Styles.defaultFonts 25 | 26 | , textAlign left 27 | , boxSizing borderBox 28 | 29 | , Css.batch ( 30 | if ifSelected then 31 | [ backgroundColor (convColor theme.bLow) 32 | , color (convColor theme.fMed) 33 | ] 34 | else 35 | [ backgroundColor unset 36 | , color (convColor theme.fLow) 37 | ] 38 | ) 39 | 40 | ] 41 | ++ styles 42 | ) 43 | ] 44 | ++ attrs 45 | ) 46 | [ Html.text label ] -------------------------------------------------------------------------------- /src/Helper/Styles.elm: -------------------------------------------------------------------------------- 1 | module Helper.Styles exposing ( globalStyles 2 | , buttonStyles 3 | , defaultFonts 4 | , cellWidth 5 | ) 6 | 7 | import Css exposing (..) 8 | import Css.Global exposing (global, selector, typeSelector) 9 | import Helper.Color exposing (convColor) 10 | import HRTheme exposing (HRTheme) 11 | import Html.Styled exposing (Html) 12 | import Rpx exposing (rpx, blc) 13 | 14 | 15 | 16 | {-| Directly copied from Parastat's UI framework. weeeeee 17 | -} 18 | globalStyles : HRTheme -> Html msg 19 | globalStyles theme = 20 | global 21 | [ selector "*" -- encourage everything to behave 22 | [ margin zero 23 | , padding zero 24 | ] 25 | 26 | , typeSelector "body" -- base properties 27 | [ displayFlex 28 | 29 | , defaultFonts 30 | , color <| convColor theme.fHigh 31 | --, lineHeight (num 1.5) 32 | , backgroundColor <| convColor theme.background 33 | 34 | ---------------------- body housecleaning ----------------------- 35 | , margin zero 36 | , padding zero 37 | 38 | , textRendering optimizeLegibility 39 | , boxSizing borderBox 40 | 41 | {- Many browser in macOS have a default font anti-aliasing 42 | style that makes fonts look thicker with rough, pixelly edges. 43 | Applying these properties makes that go away. 44 | 45 | This should be applied to the as well as anything else 46 | that happens to exist in it's own little bubble when it comes 47 | to font anti-aliasing.. 48 | -} 49 | , property "-webkit-font-smoothing" "antialiased" 50 | , property "-moz-osx-font-smoothing" "grayscale" 51 | 52 | ] 53 | 54 | ------------------- housecleaning outside of body ---------------------- 55 | 56 | -- Firefox reduces the opacity of placeholders by default. 57 | -- This forces it to not do that. 58 | , selector "*:placeholder" [ opacity (num 1) ] 59 | 60 | -- Firefox makes a bunch of awful dotted line borders 61 | -- on things that are focused on. This basically makes 62 | -- them go away. 63 | , selector "*::-moz-focus-inner" [ border zero ] 64 | 65 | , selector "@font-face" 66 | [ property "font-family" "JetBrains Mono" 67 | , property "src" "url('JetBrainsMono-Medium.woff2') format('woff2'), url('_fonts/Manrope-Medium.woff') format('woff');" 68 | , property "font-weight" "600" 69 | , property "font-style" "normal" 70 | ] 71 | 72 | ] 73 | 74 | {-| Also copied from parastat! :P 75 | -} 76 | buttonStyles : Style 77 | buttonStyles = 78 | Css.batch 79 | [ cursor pointer -- once you start messing with button styles, it becomes necessary to do this. 80 | , padding zero 81 | , margin zero 82 | , border zero 83 | , focus [ outline none ] 84 | , pseudoClass "-moz-focus-inner" [ border zero ] 85 | ] 86 | 87 | defaultFonts : Style 88 | defaultFonts = 89 | Css.batch 90 | [ fontFamilies ["JetBrains Mono", "Cousine", "Cascadia Code", "Courier", "monospace"] 91 | , fontSize (rpx 16) 92 | , fontWeight (int 600) 93 | ] 94 | 95 | 96 | cellWidth : Rem 97 | cellWidth = (blc 18) -------------------------------------------------------------------------------- /src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Browser 4 | import Color 5 | import Color.Convert 6 | import ColorMixer exposing (EditActivity(..)) 7 | import Css exposing (..) 8 | import File exposing (File) 9 | import File.Select as Select 10 | import File.Download as Download 11 | import Helper.Color exposing (convColor, getNewSelectedColor, changeSelectedColor) 12 | import Html 13 | import Html.Styled exposing (Attribute, div) 14 | import Html.Styled.Attributes exposing (css) 15 | import Html.Styled.Events exposing (..) 16 | import HRTheme exposing (HRTheme) 17 | import Json.Decode as JD 18 | import Task 19 | import Tests 20 | import Model exposing (Model, SelectedColor(..), ColorMode(..)) 21 | import Section.File 22 | import Section.Preview 23 | import Section.Mixer 24 | import Xml.Decode as XD 25 | import Helper.Styles 26 | import Rpx exposing (rpx, blc) 27 | 28 | 29 | 30 | -- MAIN 31 | 32 | 33 | main = Browser.document 34 | { init = init 35 | , view = view 36 | , update = update 37 | , subscriptions = subscriptions 38 | } 39 | 40 | 41 | {-| The example theme on the Hundred Rabbits theme git readme. 42 | -} 43 | defaultTheme : HRTheme 44 | defaultTheme = 45 | let 46 | hexConv hexStr = 47 | hexStr 48 | |> Color.Convert.hexToColor 49 | |> Result.toMaybe 50 | |> Maybe.withDefault Color.black 51 | 52 | in 53 | -- noir theme 54 | { background = hexConv "222222" 55 | , fHigh = hexConv "ffffff" 56 | , fMed = hexConv "cccccc" 57 | , fLow = hexConv "999999" 58 | , fInv = hexConv "ffffff" 59 | , bHigh = hexConv "888888" 60 | , bMed = hexConv "666666" 61 | , bLow = hexConv "444444" 62 | , bInv = hexConv "000000" 63 | } 64 | 65 | 66 | init : () -> (Model, Cmd Msg) 67 | init _ = 68 | ( { theme = defaultTheme 69 | , tests = Tests.fromTheme defaultTheme 70 | 71 | , selectedColor = Background 72 | , colorEditMode = HSL 73 | , hexInputFocused = False 74 | 75 | , mixer = ColorMixer.fromColor defaultTheme.background 76 | } 77 | , Cmd.none 78 | ) 79 | 80 | 81 | 82 | -- UPDATE 83 | 84 | 85 | type Msg 86 | = DragEnter 87 | | DragLeave 88 | 89 | | Pick 90 | | GotFiles File (List File) 91 | | ThemeLoaded String 92 | | Export 93 | 94 | | SelectedColorChanged SelectedColor 95 | | ColorHexFocusChanged Bool 96 | | ColorModeChanged ColorMode 97 | | ColorMixerEdited ColorMixer.EditActivity 98 | 99 | 100 | 101 | 102 | 103 | 104 | update : Msg -> Model -> (Model, Cmd Msg) 105 | update msg model = 106 | case msg of 107 | {- As far as I understand (not very acquainted 108 | with drag and drop atm), these are required to 109 | make drag and drop work even if you have no 110 | intention of using these. 111 | -} 112 | DragEnter -> ( model, Cmd.none ) 113 | DragLeave -> ( model, Cmd.none ) 114 | 115 | 116 | ----------------- IMPORT/EXPORT ---------------- 117 | 118 | Pick -> 119 | ( model 120 | , Select.files ["image/svg+xml"] GotFiles 121 | ) 122 | 123 | GotFiles file _ -> 124 | ( model 125 | , Task.perform ThemeLoaded (File.toString file) 126 | ) 127 | 128 | Export -> ( model, export model.theme ) 129 | 130 | ThemeLoaded fileStr -> 131 | let 132 | newThemeAttempt = XD.run HRTheme.decoder fileStr 133 | 134 | newTheme = case newThemeAttempt of 135 | Ok t -> t 136 | Err _ -> model.theme 137 | 138 | newSelectedCol = getNewSelectedColor model.selectedColor newTheme 139 | 140 | in 141 | ( { model | theme = newTheme 142 | , tests = Tests.fromTheme newTheme 143 | , mixer = ColorMixer.fromColor newSelectedCol 144 | } 145 | , Cmd.none 146 | ) 147 | 148 | 149 | ----------------- EDITING ---------------- 150 | 151 | SelectedColorChanged sc -> 152 | let 153 | newSelectedCol = getNewSelectedColor sc model.theme 154 | in 155 | ( { model | selectedColor = sc 156 | , mixer = ColorMixer.fromColor newSelectedCol 157 | } 158 | , Cmd.none 159 | ) 160 | 161 | 162 | ColorModeChanged cem -> 163 | ( { model | colorEditMode = cem } , Cmd.none ) 164 | 165 | 166 | ColorHexFocusChanged b -> 167 | if b then 168 | ( { model | hexInputFocused = b }, Cmd.none) 169 | else 170 | let 171 | newMixer = ColorMixer.edit HexDone model.mixer 172 | newTheme = changeSelectedColor newMixer.color model 173 | in 174 | ( { model | theme = newTheme 175 | , tests = Tests.fromTheme newTheme 176 | , mixer = newMixer 177 | } 178 | , Cmd.none 179 | ) 180 | 181 | 182 | ColorMixerEdited editAction -> 183 | let 184 | newMixer = ColorMixer.edit editAction model.mixer 185 | newTheme = changeSelectedColor newMixer.color model 186 | in 187 | ( { model | theme = newTheme 188 | , tests = Tests.fromTheme newTheme 189 | , mixer = newMixer 190 | } 191 | , Cmd.none 192 | ) 193 | 194 | 195 | -- SUBSCRIPTIONS 196 | 197 | 198 | subscriptions : Model -> Sub Msg 199 | subscriptions _ = 200 | Sub.none 201 | 202 | 203 | view : Model -> Browser.Document Msg 204 | view model = 205 | { title = "Hundred Rabbits theme editor" 206 | , body = [ mainView model ] 207 | } 208 | 209 | 210 | divider : HRTheme -> Html.Styled.Html Msg 211 | divider theme = 212 | div 213 | [ Html.Styled.Attributes.class "divider" 214 | , css 215 | [ width (pct 100) 216 | , height (rpx 2) 217 | , marginTop <| Rpx.subtract (blc 4) (rpx 1) 218 | , marginBottom <| Rpx.subtract (blc 4) (rpx 1) 219 | , backgroundColor (convColor theme.bLow) 220 | ] 221 | ] 222 | [] 223 | 224 | mainView : Model -> Html.Html Msg 225 | mainView model = 226 | Html.Styled.toUnstyled 227 | ( div 228 | [ css 229 | [ displayFlex 230 | , flexDirection column 231 | , alignItems center 232 | , width (vw 100) 233 | , height (vh 100) 234 | ] 235 | , hijackOn "dragenter" (JD.succeed DragEnter) 236 | , hijackOn "dragover" (JD.succeed DragEnter) 237 | , hijackOn "dragleave" (JD.succeed DragLeave) 238 | , hijackOn "drop" dropDecoder 239 | ] 240 | [ Helper.Styles.globalStyles model.theme 241 | , div 242 | [ css 243 | [ padding (blc 2) 244 | ] 245 | ] 246 | [ Section.File.view model Pick Export 247 | , divider model.theme 248 | , Section.Preview.view model SelectedColorChanged 249 | , divider model.theme 250 | , Section.Mixer.view model 251 | ColorModeChanged 252 | ColorHexFocusChanged 253 | ColorMixerEdited 254 | 255 | ] 256 | ] 257 | ) 258 | 259 | 260 | export : HRTheme -> Cmd msg 261 | export theme = 262 | Download.string "theme.svg" "image/svg+xml" (HRTheme.toXmlString theme) 263 | 264 | dropDecoder : JD.Decoder Msg 265 | dropDecoder = 266 | JD.at ["dataTransfer","files"] (JD.oneOrMore GotFiles File.decoder) 267 | 268 | 269 | hijackOn : String -> JD.Decoder msg -> Attribute msg 270 | hijackOn event decoder = 271 | preventDefaultOn event (JD.map hijack decoder) 272 | 273 | 274 | hijack : msg -> (msg, Bool) 275 | hijack msg = 276 | (msg, True) -------------------------------------------------------------------------------- /src/Model.elm: -------------------------------------------------------------------------------- 1 | module Model exposing (Model 2 | , SelectedColor(..) 3 | , ColorMode(..) 4 | ) 5 | 6 | import ColorMixer exposing (ColorMixer) 7 | import HRTheme exposing (HRTheme) 8 | import Tests exposing (Tests) 9 | 10 | 11 | type alias Model = 12 | { theme : HRTheme 13 | , selectedColor : SelectedColor 14 | , colorEditMode : ColorMode 15 | , tests : Tests 16 | 17 | , hexInputFocused : Bool 18 | , mixer : ColorMixer 19 | } 20 | 21 | type SelectedColor 22 | = Background 23 | | FHigh 24 | | FMed 25 | | FLow 26 | | FInv 27 | | BHigh 28 | | BMed 29 | | BLow 30 | | BInv 31 | 32 | type ColorMode 33 | = HSL 34 | | RGB 35 | -------------------------------------------------------------------------------- /src/Section/File.elm: -------------------------------------------------------------------------------- 1 | module Section.File exposing (view) 2 | 3 | import Css exposing (..) 4 | import Model exposing (Model) 5 | import Helper.Color exposing (convColor) 6 | import Helper.Styles exposing (cellWidth) 7 | import Helper.Icons as Icon 8 | import HRTheme exposing (HRTheme) 9 | import Html.Styled as Html exposing (Html, button, div, span) 10 | import Html.Styled.Attributes exposing (class, css) 11 | import Html.Styled.Events exposing (onClick) 12 | import Tests exposing (getThemeScore, getWCAGScoreString, getWCAGGrade, getGradationTests, GradationTest(..)) 13 | import Rpx exposing (rpx, blc) 14 | 15 | 16 | view : Model -> msg -> msg -> (Html msg) 17 | view model importMsg exportMsg = 18 | let 19 | theme = model.theme 20 | 21 | passedTestStr = 22 | case getGradationTests model.tests of 23 | BHighTooLow -> "[!] swap b_high and b_med" 24 | BMedTooLow -> "[!] swap b_med and b_low" 25 | FHighTooLow -> "[!] swap f_high and f_med" 26 | FMedTooLow -> "[!] swap f_med and f_low" 27 | Pass -> "passed!" 28 | 29 | in 30 | 31 | div [ class "files" 32 | , css 33 | [ marginTop (blc 2) 34 | , height (blc 14) 35 | , displayFlex 36 | , flexDirection row 37 | , alignItems center 38 | , justifyContent spaceBetween 39 | ] 40 | ] 41 | -------------- SCORE 42 | [ div 43 | [ class "tests" 44 | , css 45 | [ displayFlex 46 | , flexDirection column 47 | , justifyContent center 48 | 49 | , width cellWidth 50 | , height (blc 12) 51 | , marginLeft (blc 1) 52 | ] 53 | ] 54 | [ div 55 | [ css [ color (convColor model.theme.fMed) ] ] 56 | [ Html.text "theme contrast" 57 | ] 58 | , div [ ] 59 | [ span [] [ Html.text <| "[" ++ (getWCAGGrade <| getThemeScore model.tests) ++ "] " ] 60 | , span [] [ Html.text <| getWCAGScoreString <| getThemeScore model.tests ] 61 | ] 62 | , div [ css [ height (blc 2) ] ][] -- spacer 63 | , div 64 | [ css [ color (convColor model.theme.fMed) ] ] 65 | [ Html.text "basic tests" 66 | ] 67 | , div [ ] 68 | [ span [] [ Html.text passedTestStr ] 69 | ] 70 | ] 71 | 72 | -------------- FILE PREVIEW 73 | , div 74 | [ class "file-prev" 75 | , css 76 | [ border3 (rpx 1) solid (convColor model.theme.bMed) 77 | , height (rpx 64) 78 | ] 79 | ] 80 | [ Html.fromUnstyled <| HRTheme.toSvgImage model.theme ] 81 | 82 | -------------- BUTTONS 83 | , div 84 | [ class "buttons" 85 | , css 86 | [ displayFlex 87 | , flexDirection row 88 | , justifyContent flexEnd 89 | , alignItems center 90 | 91 | , width cellWidth 92 | , marginRight (blc 1) 93 | ] 94 | ] 95 | [ button 96 | [ onClick importMsg 97 | , css 98 | [ fileButtonStyles theme 99 | , marginRight (blc 2) 100 | ] 101 | ] 102 | [ Icon.importIcon ] 103 | , button 104 | [ onClick exportMsg 105 | , css 106 | [ fileButtonStyles theme 107 | ] 108 | ] 109 | [ Icon.download ] 110 | ] 111 | ] 112 | 113 | 114 | fileButtonStyles : HRTheme -> Style 115 | fileButtonStyles theme = 116 | Css.batch 117 | [ Helper.Styles.buttonStyles |> important 118 | , width (rpx (36)) 119 | , height (rpx (28)) 120 | 121 | , backgroundColor (convColor theme.background) 122 | , color (convColor theme.fLow) 123 | , hover [ color (convColor theme.fMed) ] 124 | ] 125 | -------------------------------------------------------------------------------- /src/Section/Mixer.elm: -------------------------------------------------------------------------------- 1 | module Section.Mixer exposing (view) 2 | 3 | import Helper.Color exposing (convColor) 4 | import Helper.Styles 5 | import Helper.Layout as Layout 6 | import ColorMixer exposing (ColorMixer, EditActivity(..), RGBEdit(..), HSLEdit(..)) 7 | import Css exposing (..) 8 | import Html.Styled as Html exposing (Html, div, input, label) 9 | import Html.Styled.Attributes as Attr exposing (class, css, type_, value, step) 10 | import Html.Styled.Events exposing (onClick, onInput, onFocus, onBlur) 11 | import Model exposing (Model, SelectedColor(..), ColorMode(..)) 12 | import Rpx exposing (blc, rpx) 13 | 14 | 15 | type alias EditMsg msg = EditActivity -> msg 16 | type alias ColorModeMsg msg = ColorMode -> msg 17 | type alias HexFocus msg = Bool -> msg 18 | 19 | view : Model -> ColorModeMsg msg -> HexFocus msg -> EditMsg msg -> Html msg 20 | view model colorModeMsg hexFocusMsg editMsg = 21 | div 22 | [ class "section-mixer" 23 | , css 24 | [ displayFlex 25 | , marginBottom (blc 4) 26 | ] 27 | ] 28 | [ colorArea model editMsg hexFocusMsg 29 | 30 | , div [ css [width (blc 1)]][] 31 | 32 | , div 33 | [ class "slider-area" 34 | , css 35 | [ width (blc <| (18*3) + (2*3)) ] 36 | ] 37 | [ div 38 | [ class "color-values" 39 | , css 40 | [ displayFlex 41 | , flexDirection column 42 | ] 43 | ] 44 | [ div 45 | [ class "colorMode" ] 46 | [ colorModeButton model colorModeMsg HSL "HSL" 47 | , colorModeButton model colorModeMsg RGB "RGB" 48 | ] 49 | , div 50 | [ class "sliderArea" ] 51 | [ case model.colorEditMode of 52 | HSL -> hslSliders model editMsg 53 | RGB -> rgbSliders model editMsg 54 | ] 55 | ] 56 | ] 57 | ] 58 | 59 | 60 | colorArea : Model -> EditMsg msg -> HexFocus msg -> Html msg 61 | colorArea model editMsg hexFocusMsg = 62 | let 63 | theme = model.theme 64 | label = 65 | case model.selectedColor of 66 | Background -> "background" 67 | FHigh -> "f_high" 68 | FMed -> "f_med" 69 | FLow -> "f_low" 70 | FInv -> "f_inv" 71 | BHigh -> "b_high" 72 | BMed -> "b_med" 73 | BLow -> "b_low" 74 | BInv -> "b_inv" 75 | 76 | colorPrev = model.mixer.color 77 | in 78 | div 79 | [ class "preview" 80 | , css 81 | [ displayFlex 82 | , flexDirection column 83 | , width (Rpx.add Helper.Styles.cellWidth (blc 2)) 84 | ] 85 | ] 86 | [ div 87 | [ css 88 | [ height (blc 4) 89 | , padding (blc 1) 90 | ] 91 | ] 92 | [ Html.text label ] 93 | 94 | 95 | , input 96 | [ class "hex" 97 | , type_ "text" 98 | , Attr.maxlength 7 99 | , onInput <| editMsg << HexEdited 100 | , value model.mixer.hex 101 | 102 | , onBlur <| hexFocusMsg False 103 | , onFocus <| hexFocusMsg True 104 | 105 | , css 106 | [ --- housecleaning 107 | border zero 108 | 109 | --- real styles 110 | , textBoxStyle 111 | , Helper.Styles.defaultFonts 112 | , backgroundColor (convColor theme.bHigh) 113 | , color (convColor theme.fHigh) 114 | ] 115 | ] 116 | [] 117 | 118 | 119 | , div 120 | [ class "preview" 121 | , css 122 | [ boxSizing borderBox 123 | , height (blc 11) 124 | 125 | , backgroundColor (convColor colorPrev) 126 | 127 | -- 128 | , Css.batch ( 129 | case model.selectedColor of 130 | Background -> [ border3 (rpx 1) solid (convColor theme.bMed)] 131 | _ -> [] 132 | ) 133 | ] 134 | ] 135 | [] 136 | ] 137 | 138 | colorModeButton : Model -> ColorModeMsg msg -> ColorMode -> String -> Html msg 139 | colorModeButton model msg colorMode label = 140 | Layout.cellButton 141 | model.theme 142 | ( model.colorEditMode == colorMode ) 143 | [ onClick <| msg colorMode ] 144 | [ ] 145 | label 146 | 147 | 148 | rgbSliders : Model -> EditMsg msg -> Html msg 149 | rgbSliders model editMsg = 150 | div 151 | [ css [ marginTop (blc 2) ] 152 | ] 153 | [ slider model editMsg "R" (RGBEdited << Red) 0 255 .red 154 | , slider model editMsg "G" (RGBEdited << Green) 0 255 .green 155 | , slider model editMsg "B" (RGBEdited << Blue) 0 255 .blue 156 | ] 157 | 158 | 159 | hslSliders : Model -> EditMsg msg -> Html msg 160 | hslSliders model editMsg = 161 | div 162 | [ css [ marginTop (blc 2) ] 163 | ] 164 | [ slider model editMsg "H" (HSLEdited << Hue) 0 360 .hue 165 | , slider model editMsg "S" (HSLEdited << Saturation) 0 100 .saturation 166 | , slider model editMsg "L" (HSLEdited << Lightness) 0 100 .lightness 167 | ] 168 | 169 | 170 | slider : Model 171 | -> EditMsg msg 172 | -> String 173 | -> (Float -> EditActivity) 174 | -> Int 175 | -> Int 176 | -> (ColorMixer -> Float) 177 | -> Html msg 178 | slider model editMsg labelStr editType minVal maxVal currentValAcc = 179 | let 180 | currentVal = currentValAcc model.mixer 181 | updateFunc = 182 | ColorMixer.stringToVal minVal maxVal 183 | >> editType 184 | >> editMsg 185 | 186 | valStr = ColorMixer.valToString maxVal currentVal 187 | minStr = String.fromInt minVal 188 | maxStr = String.fromInt maxVal 189 | in 190 | div 191 | [ class "sliderArea" 192 | , css 193 | [ displayFlex 194 | , flexDirection row 195 | , alignItems center 196 | , height (blc 4) 197 | , marginTop (blc 1) 198 | ] 199 | 200 | ] 201 | [ label 202 | [] 203 | [] 204 | 205 | ----------- LABEL 206 | , div 207 | [ css 208 | [ textBoxStyle 209 | , width (blc 2) 210 | , color (convColor model.theme.fMed) 211 | ] 212 | 213 | ] 214 | [Html.text labelStr] 215 | 216 | ----------- THE ACTUAL SLIDER 217 | , input 218 | [ type_ "range" 219 | , Attr.min minStr 220 | , Attr.max maxStr 221 | , onInput updateFunc 222 | , value valStr 223 | 224 | , css 225 | [ ------ housecleaning styles 226 | sliderHousecleaningStyles 227 | 228 | ------- real styles 229 | , sliderThumb 230 | [ width (blc 2) 231 | , height (blc 2) 232 | , marginTop (blc -1) -- specifying a margin is mandatory in Chrome 233 | 234 | , cursor pointer 235 | 236 | , border3 (rpx 2) solid (convColor model.theme.background) 237 | , borderRadius <| Rpx.add (blc 1) (rpx 2) 238 | , backgroundColor (convColor model.theme.fLow) 239 | ] 240 | 241 | , sliderTrack 242 | [ height (rpx 2) 243 | , color (convColor model.theme.fLow) 244 | , backgroundColor (convColor model.theme.bMed) 245 | ] 246 | ] 247 | ] 248 | [] 249 | 250 | ----------- TEXT BOX 251 | , input 252 | [ class "text" 253 | , type_ "number" 254 | , onInput updateFunc 255 | , value valStr 256 | , Attr.min minStr 257 | , Attr.max maxStr 258 | , step "1" 259 | 260 | , css 261 | [ ---- housecleaning 262 | numberHousecleaningStyles 263 | 264 | ---- normal styles 265 | , textBoxStyle 266 | , Helper.Styles.defaultFonts 267 | , color (convColor model.theme.fHigh) 268 | , width (blc 5) 269 | , marginLeft (blc 2) 270 | , backgroundColor (convColor model.theme.bHigh) 271 | ] 272 | ] 273 | [Html.text valStr] 274 | ] 275 | 276 | 277 | textBoxStyle : Style 278 | textBoxStyle = 279 | Css.batch 280 | [ displayFlex 281 | , alignItems center 282 | , padding2 zero (blc 1) 283 | , height (blc 4) 284 | ] 285 | 286 | 287 | numberHousecleaningStyles : Style 288 | numberHousecleaningStyles = 289 | Css.batch 290 | [ pseudoElement "-webkit-outer-spin-button" 291 | [ property "-webkit-appearance" "none" 292 | , margin zero 293 | ] 294 | 295 | , pseudoElement "-webkit-inner-spin-button" 296 | [ property "-webkit-appearance" "none" 297 | , margin zero 298 | ] 299 | 300 | , property "-moz-appearance" "textfield" 301 | , border zero 302 | ] 303 | 304 | sliderHousecleaningStyles : Style 305 | sliderHousecleaningStyles = 306 | Css.batch 307 | [ property "-webkit-appearance" "none" 308 | , width (pct 100) -- apparently FF needs this 309 | , backgroundColor transparent 310 | 311 | , pseudoElement "-webkit-slider-thumb" 312 | [ property "-webkit-appearance" "none" 313 | ] 314 | , pseudoElement "-ms-track" 315 | [ width (pct 100) 316 | , cursor pointer 317 | 318 | -- hides the slider so custom styles can be added 319 | , backgroundColor transparent 320 | , borderColor transparent 321 | , color transparent 322 | ] 323 | , focus 324 | [ outline none ] 325 | ] 326 | 327 | {-| Argh, it's HTML input styling time! 328 | -} 329 | sliderThumb : List Style -> Style 330 | sliderThumb styles = 331 | Css.batch 332 | [ pseudoElement "-webkit-slider-thumb" styles 333 | , pseudoElement "-moz-range-thumb" styles 334 | ] 335 | 336 | {-| Argh, it's HTML input styling time! 337 | -} 338 | sliderTrack : List Style -> Style 339 | sliderTrack styles = 340 | Css.batch 341 | [ pseudoElement "-webkit-slider-runnable-track" styles 342 | , pseudoElement "-moz-range-track" styles 343 | ] 344 | -------------------------------------------------------------------------------- /src/Section/Preview.elm: -------------------------------------------------------------------------------- 1 | module Section.Preview exposing (view) 2 | 3 | import Css exposing (..) 4 | import Color exposing (Color) 5 | import Helper.Color exposing (convColor) 6 | import Helper.Styles exposing (cellWidth) 7 | import Helper.Layout as Layout 8 | import Html.Styled as Html exposing (Html, div, span) 9 | import Html.Styled.Attributes exposing (css, class) 10 | import Html.Styled.Events exposing (onClick) 11 | import Model exposing (Model, SelectedColor(..)) 12 | import Rpx exposing (blc) 13 | import Tests exposing (getWCAGGrade, getWCAGScoreString, GradationTest(..), getGradationTests) 14 | 15 | 16 | 17 | 18 | view : Model -> (SelectedColor -> msg) -> Html msg 19 | view model selectMsg = 20 | let 21 | theme = model.theme 22 | tests = model.tests 23 | in 24 | div 25 | [ css 26 | [ displayFlex 27 | , flexDirection row 28 | ] 29 | , class "preview" 30 | ] 31 | 32 | ------------- left area 33 | [ smallerBlocking 34 | [ blankCell [] 35 | 36 | , div [ css [ height (blc 1) ]][] -- spacer 37 | 38 | , paletteButton model "background" selectMsg Background "" 39 | , paletteButton model "b_low" selectMsg BLow 40 | ( gradation [(BMedTooLow, "↑")] model ) 41 | , paletteButton model "b_med" selectMsg BMed 42 | ( gradation [(BMedTooLow, "↓"), (BHighTooLow, "↑")] model) 43 | , paletteButton model "b_high" selectMsg BHigh 44 | ( gradation [(BHighTooLow, "↓")] model ) 45 | 46 | , div [ css [ height (blc 2) ] ][] -- spacer 47 | 48 | , paletteButton model "b_inv" selectMsg BInv "" 49 | ] 50 | 51 | , div [ css [ width (blc 1) ] ][] -- spacer 52 | 53 | ------------- right area 54 | , largerBlocking 55 | [ buttonRow 56 | [ paletteButton model "f_high" selectMsg FHigh 57 | ( gradation [(FHighTooLow, "←")] model ) 58 | , paletteButton model "f_med" selectMsg FMed 59 | ( gradation [(FHighTooLow, "→"), (FMedTooLow, "←")] model) 60 | , paletteButton model "f_low" selectMsg FLow 61 | ( gradation [(FMedTooLow, "→")] model ) 62 | ] 63 | 64 | , div [ css [ height (blc 1) ]][] -- spacer 65 | 66 | , paletteRow 67 | [ paletteCircle tests.contrastBgFHigh theme.background theme.fHigh 68 | , paletteCircle tests.contrastBgFMed theme.background theme.fMed 69 | , paletteCircle tests.contrastBgFLow theme.background theme.fLow 70 | ] 71 | 72 | , paletteRow 73 | [ paletteCircle tests.contrastBLowFHigh theme.bLow theme.fHigh 74 | , paletteCircle tests.contrastBLowFMed theme.bLow theme.fMed 75 | , paletteCircle tests.contrastBLowFLow theme.bLow theme.fLow 76 | ] 77 | 78 | , paletteRow 79 | [ paletteCircle tests.contrastBMedFHigh theme.bMed theme.fHigh 80 | , paletteCircle tests.contrastBMedFMed theme.bMed theme.fMed 81 | , paletteCircle tests.contrastBMedFLow theme.bMed theme.fLow 82 | ] 83 | 84 | , paletteRow 85 | [ paletteCircle tests.contrastBHighFHigh theme.bHigh theme.fHigh 86 | , paletteCircle tests.contrastBHighFMed theme.bHigh theme.fMed 87 | , paletteCircle tests.contrastBHighFLow theme.bHigh theme.fLow 88 | ] 89 | 90 | , div [ css [ height (blc 2) ]][] -- spacer 91 | 92 | , paletteRow 93 | [ paletteCircle tests.contrastBInvFInv theme.bInv theme.fInv 94 | ] 95 | 96 | , div [ css [ height (blc 1) ]][] -- spacer 97 | 98 | , buttonRow 99 | [ paletteButton model "f_inv" selectMsg FInv "" 100 | ] 101 | ] 102 | 103 | ] 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {-| Provides a grade colour based on the 112 | background color for best possible contrast 113 | for the WCAG contrast grades. 114 | -} 115 | gradeColor : Color -> Color 116 | gradeColor bgCol = 117 | let 118 | lightness = 119 | bgCol 120 | |> Color.toHsla 121 | |> .lightness 122 | in 123 | if lightness < 0.5 then 124 | Color.rgb255 255 255 255 125 | else 126 | Color.rgb255 0 0 0 127 | 128 | 129 | 130 | gradation : List (GradationTest, String) -> Model -> String 131 | gradation results model = 132 | let 133 | gradationTestsMet = List.filter (\x -> getGradationTests model.tests == Tuple.first x) results 134 | in 135 | case List.head gradationTestsMet of 136 | Nothing -> "" 137 | Just h -> "[" ++ Tuple.second h ++ "]" 138 | 139 | 140 | smallerBlocking : List (Html msg) -> Html msg 141 | smallerBlocking content = 142 | div 143 | [ css 144 | [ displayFlex 145 | , flexDirection column 146 | , minWidth cellWidth 147 | ] 148 | ] 149 | content 150 | 151 | 152 | {-| To keep the different areas of the editor looking nice, 153 | there are two blocking areas, the larger one on the right, 154 | and the smaller one on the left, this is the larger of 155 | the two. 156 | -} 157 | largerBlocking : List (Html msg) -> Html msg 158 | largerBlocking content = 159 | div 160 | [ css 161 | [ displayFlex 162 | , flexDirection column 163 | , minWidth (blc <| 18 * 3) 164 | ] 165 | ] 166 | content 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | blankCell : List (Html msg) -> Html msg 177 | blankCell content = 178 | div 179 | [ css -- spacing area 180 | [ minWidth cellWidth 181 | , height (blc 3) 182 | , padding (blc 1) 183 | ] 184 | ] 185 | content 186 | 187 | 188 | 189 | paletteButton : Model -> String -> (SelectedColor -> msg) -> SelectedColor -> String -> Html msg 190 | paletteButton model label msg thisColor endStr = 191 | Layout.cellButton 192 | model.theme 193 | ( model.selectedColor == thisColor ) 194 | [ onClick <| msg thisColor ] 195 | [ ] 196 | ( label ++ " " ++ endStr ) 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | buttonRow : List (Html msg) -> Html msg 211 | buttonRow content = 212 | div 213 | [ css 214 | [ displayFlex 215 | ] 216 | ] 217 | content 218 | 219 | 220 | 221 | 222 | paletteCircle : Float -> Color -> Color -> Html msg 223 | paletteCircle contrastScore bg fg = 224 | div 225 | [ css 226 | [ displayFlex 227 | , flexDirection row 228 | , minWidth (blc 18) 229 | , height (blc 3) 230 | , padding (blc 1) 231 | , backgroundColor (convColor bg) 232 | ] 233 | ] 234 | [ div -- circle 235 | [ css 236 | [ width (blc 3) 237 | , height (blc 3) 238 | , backgroundColor (convColor fg) 239 | , borderRadius (blc 2) 240 | ] 241 | ] 242 | [] 243 | , span -- accessibility score 244 | [ css 245 | [ marginLeft (blc 1) 246 | , color (convColor fg) 247 | ] 248 | ] 249 | [ Html.text <| getWCAGScoreString contrastScore ] 250 | 251 | , span -- accessibility grade 252 | [ css 253 | [ marginLeft (blc 1) 254 | , color (convColor <| gradeColor bg) 255 | ] 256 | ] 257 | [ Html.text <| "[" ++ (getWCAGGrade contrastScore) ++ "]" ] 258 | ] 259 | 260 | paletteRow : List (Html msg) -> Html msg 261 | paletteRow content = 262 | div [ css 263 | [ displayFlex 264 | , flex none 265 | , flexDirection row 266 | , alignItems center 267 | ] 268 | , class "palette-box" 269 | ] 270 | content 271 | -------------------------------------------------------------------------------- /src/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing ( Tests 2 | , fromTheme 3 | 4 | , getThemeScore 5 | , getWCAGScoreString 6 | , getWCAGGrade 7 | 8 | , GradationTest(..) 9 | , getGradationTests 10 | ) 11 | 12 | import Color.Accessibility exposing (contrastRatio) 13 | import HRTheme exposing (HRTheme) 14 | 15 | {-| A data structure encapsulating all possible theme tests. 16 | -} 17 | type alias Tests = 18 | { ----------- Contrast against BG tests 19 | contrastBgBMed : Float 20 | , contrastBgBHigh : Float 21 | , contrastBgBLow : Float 22 | 23 | -------- WCAG Tests + BG tests 24 | , contrastBgFHigh : Float 25 | , contrastBgFMed : Float 26 | , contrastBgFLow : Float 27 | 28 | ------- All WCAG tests 29 | , contrastBLowFHigh : Float 30 | , contrastBLowFMed : Float 31 | , contrastBLowFLow : Float 32 | 33 | , contrastBMedFHigh : Float 34 | , contrastBMedFMed : Float 35 | , contrastBMedFLow : Float 36 | 37 | , contrastBHighFHigh : Float 38 | , contrastBHighFMed : Float 39 | , contrastBHighFLow : Float 40 | 41 | , contrastBInvFInv : Float 42 | } 43 | 44 | 45 | {-| Takes an HRTheme and returns a full slate of tests. 46 | -} 47 | fromTheme : HRTheme -> Tests 48 | fromTheme t = 49 | { ----------- Contrast against BG tests 50 | contrastBgBHigh = contrastRatio t.background t.bHigh 51 | , contrastBgBMed = contrastRatio t.background t.bMed 52 | , contrastBgBLow = contrastRatio t.background t.bLow 53 | 54 | -------- WCAG Tests + BG tests 55 | , contrastBgFHigh = contrastRatio t.background t.fHigh 56 | , contrastBgFMed = contrastRatio t.background t.fMed 57 | , contrastBgFLow = contrastRatio t.background t.fLow 58 | 59 | ------- All WCAG tests 60 | , contrastBLowFHigh = contrastRatio t.bLow t.fHigh 61 | , contrastBLowFMed = contrastRatio t.bLow t.fMed 62 | , contrastBLowFLow = contrastRatio t.bLow t.fLow 63 | 64 | , contrastBMedFHigh = contrastRatio t.bMed t.fHigh 65 | , contrastBMedFMed = contrastRatio t.bMed t.fMed 66 | , contrastBMedFLow = contrastRatio t.bMed t.fLow 67 | 68 | , contrastBHighFHigh = contrastRatio t.bHigh t.fHigh 69 | , contrastBHighFMed = contrastRatio t.bHigh t.fMed 70 | , contrastBHighFLow = contrastRatio t.bHigh t.fLow 71 | 72 | , contrastBInvFInv = contrastRatio t.bInv t.fInv 73 | } 74 | 75 | 76 | {-| Returns the score of the lowest scoring contrast combo. 77 | -} 78 | getThemeScore : Tests -> Float 79 | getThemeScore t = 80 | let 81 | calcs = 82 | [ t.contrastBgFHigh 83 | , t.contrastBgFMed 84 | , t.contrastBgFLow 85 | 86 | ------- All WCAG tests 87 | , t.contrastBLowFHigh 88 | , t.contrastBLowFMed 89 | , t.contrastBLowFLow 90 | 91 | , t.contrastBMedFHigh 92 | , t.contrastBMedFMed 93 | , t.contrastBMedFLow 94 | 95 | , t.contrastBHighFHigh 96 | , t.contrastBHighFMed 97 | , t.contrastBHighFLow 98 | 99 | , t.contrastBInvFInv 100 | ] 101 | 102 | in 103 | -- assume it will work, because there's no way it can't 104 | Maybe.withDefault 0.1 <| List.minimum calcs 105 | 106 | 107 | 108 | 109 | {-| Returns a presentable WCAG contrast score, showing only one decimal place. 110 | 111 | (Floats are fickle when presenting in web browsers.) 112 | -} 113 | getWCAGScoreString : Float -> String 114 | getWCAGScoreString accScore = 115 | let 116 | inted = accScore 117 | |> (*) 10 118 | |> truncate 119 | |> String.fromInt 120 | in 121 | String.slice 0 -1 inted ++ "." ++ String.right 1 inted 122 | 123 | 124 | 125 | {-| Returns a grade consistent with WCAG 2.0 standards 126 | based on the contrast ratio given. 127 | 128 | (Apart from X, that's just something I came up with to 129 | denote those that don't pass minimum standards.) 130 | -} 131 | getWCAGGrade : Float -> String 132 | getWCAGGrade accScore = 133 | if accScore < 3 then 134 | "X" 135 | else if accScore >= 3 && accScore < 4.5 then 136 | "A" 137 | else if accScore >= 4.5 && accScore < 7 then 138 | "AA" 139 | else -- if accScore >= 7 then 140 | "AAA" 141 | 142 | 143 | type GradationTest 144 | = BHighTooLow 145 | | BMedTooLow 146 | | FHighTooLow 147 | | FMedTooLow 148 | | Pass 149 | 150 | getGradationTests : Tests -> GradationTest 151 | getGradationTests t = 152 | if t.contrastBgBHigh < t.contrastBgBMed then 153 | BHighTooLow 154 | else if t.contrastBgBMed < t.contrastBgBLow then 155 | BMedTooLow 156 | else if t.contrastBgFHigh < t.contrastBgFMed then 157 | FHighTooLow 158 | else if t.contrastBgFMed < t.contrastBgFLow then 159 | FMedTooLow 160 | else 161 | Pass --------------------------------------------------------------------------------