├── .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 | 
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
--------------------------------------------------------------------------------