colorsbymax 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -138
- package/THIRD_PARTY_NOTICES.md +47 -1
- package/dist/index.js +220 -18
- package/dist/pdf.js +9 -0
- package/package.json +37 -9
- package/types/index.d.ts +271 -0
- package/types/pdf.d.ts +7 -0
package/README.md
CHANGED
|
@@ -1,138 +1,164 @@
|
|
|
1
|
-
# colorsbymax™
|
|
2
|
-
|
|
3
|
-
**by mrmaxdesigns**
|
|
4
|
-
|
|
5
|
-
A floating theme switcher for websites. Visitors (or the site's owner) can re-colour the whole site instantly: pick one of the site's own themes, a hand-tuned pick, or one of 715 library themes in 14 categories; build and share custom palettes; or override single colours. Every theme is checked against the Web Content Accessibility Guidelines (WCAG) contrast rules, with one-click fixes.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
##
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
npm
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
# colorsbymax™
|
|
2
|
+
|
|
3
|
+
**by mrmaxdesigns**
|
|
4
|
+
|
|
5
|
+
A floating theme switcher for websites. Visitors (or the site's owner) can re-colour the whole site instantly: pick one of the site's own themes, a hand-tuned pick, or one of 715 library themes in 14 categories; build and share custom palettes; or override single colours. Every theme is checked against the Web Content Accessibility Guidelines (WCAG) contrast rules, with one-click fixes.
|
|
6
|
+
|
|
7
|
+
**At a glance**
|
|
8
|
+
|
|
9
|
+
- **React 18 or 19.** `npm install colorsbymax`, then wrap your app (see [Add it to a site](#add-it-to-a-site)).
|
|
10
|
+
- **Tailwind optional.** The panel carries its own styles, so it works with Tailwind v4, older Tailwind or plain CSS. Your site only needs to paint its colours with `var(--color-…)` variables. The optional `colorsbymax/tokens.css` helper is for Tailwind v4 (`@theme` syntax); without Tailwind v4, define the variables yourself.
|
|
11
|
+
- **TypeScript types included.**
|
|
12
|
+
- **No runtime dependencies** besides React. PDF uploads are opt-in and need `pdfjs-dist` (see [PDF uploads](#pdf-uploads)).
|
|
13
|
+
- **ESM only.** Import it from a bundler or `import()`; `require('colorsbymax')` from CommonJS isn't supported.
|
|
14
|
+
- **The colour tools work on their own too.** `contrastRatio`, `checkTheme`, `suggestFix`, `fixAll`, `themeFromPalette`, `darkTokens` and the rest are plain functions with no UI.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Site themes first.** The panel opens on a group named after the site, holding its own colours. If those fail contrast, an accessible version is generated automatically.
|
|
19
|
+
- **Scan the site.** Press "Scan site" and colorsbymax reads the colours actually painted on the page (ignoring any theme it has applied, and including gradients). It works out the page background, surfaces, text and brand colours, then adds themes named after the site: Scanned (as found), Accessible, Soft, Bold, Complementary, and the closest library matches. Scans run only when asked, and the results are remembered.
|
|
20
|
+
- **Max’s picks and library.** 5 hand-tuned picks, plus 715 library themes (Bright, Fun, Pastel, Earth tones, Summer, Autumn, Winter, Spring, Ocean, Warm, Nature, Moody, Monochrome, Eclectic) with search and "Surprise me". The library loads only when the panel opens.
|
|
21
|
+
- **Custom palettes, single-colour overrides, JSON import/export.**
|
|
22
|
+
- **Palettes from images and PDFs.** In Import / export, upload or drop a mood board, screenshot or photo. colorsbymax picks out its main colours, lets you leave any out, previews the palette it builds around them and saves it as a custom palette. Nothing leaves the browser. PDFs, such as brand guides, work where the site [turns them on](#pdf-uploads); hex codes written in a PDF take priority.
|
|
23
|
+
- **Contrast checks.** Problems show as a badge on the theme; the breakdown offers per-item fixes or "Fix all automatically", which changes lightness only.
|
|
24
|
+
- **No flash on reload.** An inline pre-paint script applies the saved theme before the page draws.
|
|
25
|
+
- **Works on any site.** The panel carries its own stylesheet inside a shadow root, so it needs no Tailwind or other CSS from the site, and the site's CSS can't restyle it.
|
|
26
|
+
- **Movable colour button.** The floating button starts in the corner; visitors can drag it anywhere (mouse or touch; a tooltip says so on hover, and the panel closes while it moves) and it stays there, remembered across reloads. The panel then opens beside it, on whichever side has room. Its dot cycles through the current theme's colours.
|
|
27
|
+
- **Light and dark mode.** Every built-in theme is designed light; dark mode lists a generated dark twin of each (dark surfaces, light text, brand colours lifted to read on dark, then contrast-fixed) and switches the current theme to its twin. The panel turns dark with it. Custom palettes stay as they were made.
|
|
28
|
+
- **Visitor settings.** The gear in the panel header opens settings: theme mode (Light, Dark, Auto), panel size (Compact, Standard, Large), which groups and sections to show, whether the button can be dragged or its dot animates, and moving the button back to its corner. Saved per site.
|
|
29
|
+
- **Resizable panel.** Drag the panel's free edges or corner (the ones away from the colour button) to any size, or pick a size in settings; double-click an edge to reset it. The layout follows the panel's width, so a large panel shows three theme cards a row.
|
|
30
|
+
- **Clear groups and feedback.** The site's own group (globe), Max’s picks (paintbrush) and Yours (person) sit in their own row, apart from the library's categories. Toasts confirm what just happened; saving, importing or building a palette says it went to Yours and offers "Show" to jump straight to it. Tooltips are drawn in the panel's colours.
|
|
31
|
+
- **Themed scrollbars.** The page's scrollbars and the panel's slim one take the selected theme's primary colour. Turn the page's off with `scrollbars: false`.
|
|
32
|
+
- **Accessible panel.** A labelled dialog with focus trap, Escape, the colour button or an outside click to close, keyboard operable, and reduced-motion support.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
Every colour is one of 35 tokens (`primary`, `surface`, `ink`, `data-1`…), exposed as `--color-<token>` CSS variables. Applying a theme just sets those variables on `<html>`, so anything the site paints with `var(--color-primary)` (directly, or through Tailwind CSS v4 utilities like `bg-primary`) changes instantly. There's no rebuild and no re-render.
|
|
37
|
+
|
|
38
|
+
The switcher renders into a `<colorsbymax-root>` element on `<body>` with its own shadow root and stylesheet. Only the `--color-*` variables cross into it. With reduced motion, the colour button's dot holds still on the theme's primary colour.
|
|
39
|
+
|
|
40
|
+
## Try the demo
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install
|
|
44
|
+
npm run dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This serves `demo/`: colorsbymax's own landing page, built with Tailwind, where every colour is a token (it uses every token group, and a strip shows the live values), and `/plain.html`, a plain-CSS bakery site with deliberately careless global styles to show they don't reach the panel.
|
|
48
|
+
|
|
49
|
+
## Add it to a site
|
|
50
|
+
|
|
51
|
+
colorsbymax needs React 18 or 19:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install colorsbymax
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It ships as plain JavaScript with TypeScript types, so Vite, Next.js, webpack and other bundlers use it without extra setup. Installing changes nothing on its own; these steps add the colour button:
|
|
58
|
+
|
|
59
|
+
1. **Paint the site with the token variables.** Use `var(--color-<token>)` wherever the site sets a colour, with your own colours as the starting values.
|
|
60
|
+
|
|
61
|
+
With Tailwind CSS v4, import the defaults (`colorsbymax/tokens.css` is Tailwind v4 syntax), override them, and use the token utilities (`bg-primary`, `text-ink`, …) instead of hard-coded colours:
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
@import "tailwindcss";
|
|
65
|
+
@import "colorsbymax/tokens.css";
|
|
66
|
+
|
|
67
|
+
@theme static {
|
|
68
|
+
--color-primary: #c67cde; /* your colours */
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
With plain CSS, or Tailwind before v4, define the variables yourself (`demo/plain.html` shows this):
|
|
73
|
+
|
|
74
|
+
```css
|
|
75
|
+
:root {
|
|
76
|
+
--color-primary: #c67cde;
|
|
77
|
+
}
|
|
78
|
+
.button {
|
|
79
|
+
background: var(--color-primary);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
2. **Wrap the app and render the switcher:**
|
|
84
|
+
|
|
85
|
+
```jsx
|
|
86
|
+
import { ThemeProvider, ThemeSwitcher } from 'colorsbymax'
|
|
87
|
+
|
|
88
|
+
<ThemeProvider config={config}>
|
|
89
|
+
<App />
|
|
90
|
+
<ThemeSwitcher />
|
|
91
|
+
</ThemeProvider>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The switcher needs React but not a React site: `demo/plain.jsx` mounts it on its own next to a static page.
|
|
95
|
+
|
|
96
|
+
3. **Add the pre-paint script** to `<head>`, as a classic inline `<script>`, using the same storage key. `prePaintScript(storageKey)` returns its source, and `demo/index.html` shows it in place.
|
|
97
|
+
|
|
98
|
+
Without a `siteName`, the site group is named from the page's `og:site_name`, its title or its host name.
|
|
99
|
+
|
|
100
|
+
### Config
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
{
|
|
104
|
+
siteName: 'Tsungi', // name of the first theme group
|
|
105
|
+
storageKey: 'tsungi-theme', // localStorage key (match the pre-paint script)
|
|
106
|
+
defaultTheme: { name, tokens }, // the site's own colours; missing tokens are filled in
|
|
107
|
+
themes: [{ id, name, tokens }], // optional extra themes made for the site
|
|
108
|
+
usage: { primary: 'Buttons…' }, // optional notes shown in the colour editors
|
|
109
|
+
scrollbars: true, // colour the page's scrollbars from the theme (default)
|
|
110
|
+
pdf: loadPdf, // optional: allow PDF uploads (see below)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### PDF uploads
|
|
115
|
+
|
|
116
|
+
Building a palette from an image works out of the box. PDFs, such as brand guides, need [PDF.js](https://mozilla.github.io/pdf.js/), which is large, so it's opt-in: install it and pass the loader from `colorsbymax/pdf`.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install pdfjs-dist
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```jsx
|
|
123
|
+
import { loadPdf } from 'colorsbymax/pdf'
|
|
124
|
+
|
|
125
|
+
<ThemeProvider config={{ ...config, pdf: loadPdf }}>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
PDF.js still downloads only when a visitor picks a PDF. Sites that don't opt in never install or bundle it, and the upload offers images only.
|
|
129
|
+
|
|
130
|
+
`examples/tsungi.config.js` is a complete example for tsungi.online, the first site to use colorsbymax.
|
|
131
|
+
|
|
132
|
+
## Building the package
|
|
133
|
+
|
|
134
|
+
`src/` is the source; `dist/` is what sites install: plain JavaScript with the JSX compiled away. `types/` holds the hand-written TypeScript declarations; `npm run typecheck` compiles `types/check.tsx` against them and checks they match the built exports. The panel's icons are copied from Lucide into `src/icons.jsx` by `npm run icons`. `dist/` is committed so installs straight from GitHub work even when npm skips install scripts, so rebuild it before committing changes to `src/`. `npm publish` also rebuilds it first:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm run build
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Panel styles
|
|
141
|
+
|
|
142
|
+
The panel is styled with Tailwind classes in `src/ThemePanel.jsx` and `src/ThemeSwitcher.jsx`. `scripts/build-css.mjs` compiles them, with `src/panel.css`, into `src/panel-css.generated.js`. The demo server does this automatically as you edit; otherwise run:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run css
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Regenerating the library
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm run presets
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`scripts/generate-presets.mjs` turns each source palette into a full theme, auto-fixes contrast, drops anything that still fails or duplicates another theme, names it and tags its categories. Adjust the category rules at the top of the script.
|
|
155
|
+
|
|
156
|
+
## Notices
|
|
157
|
+
|
|
158
|
+
The library palettes come from [nice-color-palettes](https://github.com/Jam3/nice-color-palettes) (MIT). Its licence notice is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and must ship with every copy.
|
|
159
|
+
|
|
160
|
+
PDF reading uses [pdfjs-dist](https://github.com/mozilla/pdf.js) (Apache-2.0), installed as a dependency rather than bundled into colorsbymax.
|
|
161
|
+
|
|
162
|
+
## Licence
|
|
163
|
+
|
|
164
|
+
colorsbymax is released under the [MIT Licence](LICENSE). The names colorsbymax™ and mrmaxdesigns are marks of Max Muyalwa and aren't covered by the code licence.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-party notices
|
|
2
2
|
|
|
3
|
-
colorsbymax™ includes material from the following
|
|
3
|
+
colorsbymax™ includes material from the following projects. Their licences require these notices to ship with every copy of the product.
|
|
4
4
|
|
|
5
5
|
## nice-color-palettes
|
|
6
6
|
|
|
@@ -29,3 +29,49 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
|
29
29
|
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
+
|
|
33
|
+
## Lucide
|
|
34
|
+
|
|
35
|
+
The switcher's icons (`src/icons.jsx`) are copied from [Lucide](https://lucide.dev), generated from lucide-react by `scripts/build-icons.mjs`. Some Lucide icons derive from [Feather](https://feathericons.com); of those used here, that's arrow-left, check, chevron-right, download, monitor, moon, trash, upload and x, covered by the MIT notice below.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
ISC License
|
|
39
|
+
|
|
40
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
41
|
+
|
|
42
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
43
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
44
|
+
copyright notice and this permission notice appear in all copies.
|
|
45
|
+
|
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
47
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
48
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
49
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
50
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
51
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
52
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
The MIT License (MIT) (for the icons listed above)
|
|
57
|
+
|
|
58
|
+
Copyright (c) 2013-present Cole Bemis
|
|
59
|
+
|
|
60
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
61
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
62
|
+
in the Software without restriction, including without limitation the rights
|
|
63
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
64
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
65
|
+
furnished to do so, subject to the following conditions:
|
|
66
|
+
|
|
67
|
+
The above copyright notice and this permission notice shall be included in all
|
|
68
|
+
copies or substantial portions of the Software.
|
|
69
|
+
|
|
70
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
71
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
72
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
73
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
74
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
75
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
76
|
+
SOFTWARE.
|
|
77
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import { AlertTriangle, ArrowLeft, Check, CheckCircle2, ChevronRight, Copy, Download, Globe, ImageUp, Loader2, Monitor, Moon, Paintbrush, Palette, RotateCcw, ScanLine, Settings, Shuffle, Sun, Trash2, Upload, UserRound, Wand2, X } from "lucide-react";
|
|
5
4
|
//#region src/color.js
|
|
6
5
|
var HEX_RE = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
7
6
|
/** Normalises "#abc", "abc", "#AABBCC" to "#aabbcc"; returns null for anything else. */
|
|
@@ -1062,6 +1061,9 @@ function createColorParser() {
|
|
|
1062
1061
|
* colours. Everything happens synchronously, so nothing repaints in between.
|
|
1063
1062
|
*/
|
|
1064
1063
|
function withoutAppliedTheme(fn) {
|
|
1064
|
+
const freeze = document.createElement("style");
|
|
1065
|
+
freeze.textContent = "*,*::before,*::after{transition:none!important}";
|
|
1066
|
+
document.head.appendChild(freeze);
|
|
1065
1067
|
const style = document.documentElement.style;
|
|
1066
1068
|
const saved = [];
|
|
1067
1069
|
for (let i = style.length - 1; i >= 0; i--) {
|
|
@@ -1075,6 +1077,9 @@ function withoutAppliedTheme(fn) {
|
|
|
1075
1077
|
return fn();
|
|
1076
1078
|
} finally {
|
|
1077
1079
|
for (const [prop, value] of saved) style.setProperty(prop, value);
|
|
1080
|
+
getComputedStyle(document.documentElement).color;
|
|
1081
|
+
document.body.offsetHeight;
|
|
1082
|
+
freeze.remove();
|
|
1078
1083
|
}
|
|
1079
1084
|
}
|
|
1080
1085
|
/**
|
|
@@ -1423,6 +1428,7 @@ var newId = () => `custom-${Date.now().toString(36)}-${Math.random().toString(36
|
|
|
1423
1428
|
* @property {Partial<Record<import('./tokens.js').TokenKey, string>>} [usage]
|
|
1424
1429
|
* Where each token is used on this site, shown in the editors
|
|
1425
1430
|
* @property {boolean} [scrollbars] Colour the page's scrollbars from the theme (default true)
|
|
1431
|
+
* @property {() => Promise<any>} [pdf] Enables PDF uploads: pass `loadPdf` from 'colorsbymax/pdf'
|
|
1426
1432
|
*/
|
|
1427
1433
|
/** Resolves a config into the site theme group. The shipped default always comes first. */
|
|
1428
1434
|
function resolveSite(config) {
|
|
@@ -1529,6 +1535,7 @@ function ThemeProvider({ config = {}, children }) {
|
|
|
1529
1535
|
storageKey,
|
|
1530
1536
|
siteName,
|
|
1531
1537
|
usage: config.usage ?? {},
|
|
1538
|
+
loadPdf: config.pdf ?? null,
|
|
1532
1539
|
siteThemes,
|
|
1533
1540
|
defaultTheme,
|
|
1534
1541
|
presets: PRESETS,
|
|
@@ -1683,6 +1690,193 @@ function useTheme() {
|
|
|
1683
1690
|
if (!ctx) throw new Error("useTheme must be used inside <ThemeProvider>");
|
|
1684
1691
|
return ctx;
|
|
1685
1692
|
}
|
|
1693
|
+
//#endregion
|
|
1694
|
+
//#region src/icons.jsx
|
|
1695
|
+
/** A Lucide-style icon: 24×24, drawn with currentColor strokes, sized by className. */
|
|
1696
|
+
function icon(slug, shapes) {
|
|
1697
|
+
const Icon = ({ className = "", ...props }) => /* @__PURE__ */ jsx("svg", {
|
|
1698
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
1699
|
+
width: "24",
|
|
1700
|
+
height: "24",
|
|
1701
|
+
viewBox: "0 0 24 24",
|
|
1702
|
+
fill: "none",
|
|
1703
|
+
stroke: "currentColor",
|
|
1704
|
+
strokeWidth: "2",
|
|
1705
|
+
strokeLinecap: "round",
|
|
1706
|
+
strokeLinejoin: "round",
|
|
1707
|
+
className: `lucide lucide-${slug} ${className}`.trim(),
|
|
1708
|
+
...props,
|
|
1709
|
+
children: shapes.map(([tag, attrs], i) => createElement(tag, {
|
|
1710
|
+
key: i,
|
|
1711
|
+
...attrs
|
|
1712
|
+
}))
|
|
1713
|
+
});
|
|
1714
|
+
Icon.displayName = slug;
|
|
1715
|
+
return Icon;
|
|
1716
|
+
}
|
|
1717
|
+
var AlertTriangle = icon("triangle-alert", [
|
|
1718
|
+
["path", { "d": "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }],
|
|
1719
|
+
["path", { "d": "M12 9v4" }],
|
|
1720
|
+
["path", { "d": "M12 17h.01" }]
|
|
1721
|
+
]);
|
|
1722
|
+
var ArrowLeft = icon("arrow-left", [["path", { "d": "m12 19-7-7 7-7" }], ["path", { "d": "M19 12H5" }]]);
|
|
1723
|
+
var Check = icon("check", [["path", { "d": "M20 6 9 17l-5-5" }]]);
|
|
1724
|
+
var CheckCircle2 = icon("circle-check", [["circle", {
|
|
1725
|
+
"cx": "12",
|
|
1726
|
+
"cy": "12",
|
|
1727
|
+
"r": "10"
|
|
1728
|
+
}], ["path", { "d": "m16 9-5.5 5.5L8 12" }]]);
|
|
1729
|
+
var ChevronRight = icon("chevron-right", [["path", { "d": "m9 18 6-6-6-6" }]]);
|
|
1730
|
+
var Copy = icon("copy", [["rect", {
|
|
1731
|
+
"width": "14",
|
|
1732
|
+
"height": "14",
|
|
1733
|
+
"x": "8",
|
|
1734
|
+
"y": "8",
|
|
1735
|
+
"rx": "2",
|
|
1736
|
+
"ry": "2"
|
|
1737
|
+
}], ["path", { "d": "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]]);
|
|
1738
|
+
var Download = icon("download", [
|
|
1739
|
+
["path", { "d": "M12 15V3" }],
|
|
1740
|
+
["path", { "d": "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
|
1741
|
+
["path", { "d": "m7 10 5 5 5-5" }]
|
|
1742
|
+
]);
|
|
1743
|
+
var Globe = icon("globe", [
|
|
1744
|
+
["circle", {
|
|
1745
|
+
"cx": "12",
|
|
1746
|
+
"cy": "12",
|
|
1747
|
+
"r": "10"
|
|
1748
|
+
}],
|
|
1749
|
+
["path", { "d": "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }],
|
|
1750
|
+
["path", { "d": "M2 12h20" }]
|
|
1751
|
+
]);
|
|
1752
|
+
var ImageUp = icon("image-up", [
|
|
1753
|
+
["path", { "d": "M10.3 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v10l-3.1-3.1a2 2 0 0 0-2.814.014L6 21" }],
|
|
1754
|
+
["path", { "d": "m14 19.5 3-3 3 3" }],
|
|
1755
|
+
["path", { "d": "M17 22v-5.5" }],
|
|
1756
|
+
["circle", {
|
|
1757
|
+
"cx": "9",
|
|
1758
|
+
"cy": "9",
|
|
1759
|
+
"r": "2"
|
|
1760
|
+
}]
|
|
1761
|
+
]);
|
|
1762
|
+
var Loader2 = icon("loader-circle", [["path", { "d": "M21 12a9 9 0 1 1-6.219-8.56" }]]);
|
|
1763
|
+
var Monitor = icon("monitor", [
|
|
1764
|
+
["rect", {
|
|
1765
|
+
"width": "20",
|
|
1766
|
+
"height": "14",
|
|
1767
|
+
"x": "2",
|
|
1768
|
+
"y": "3",
|
|
1769
|
+
"rx": "2"
|
|
1770
|
+
}],
|
|
1771
|
+
["line", {
|
|
1772
|
+
"x1": "8",
|
|
1773
|
+
"x2": "16",
|
|
1774
|
+
"y1": "21",
|
|
1775
|
+
"y2": "21"
|
|
1776
|
+
}],
|
|
1777
|
+
["line", {
|
|
1778
|
+
"x1": "12",
|
|
1779
|
+
"x2": "12",
|
|
1780
|
+
"y1": "17",
|
|
1781
|
+
"y2": "21"
|
|
1782
|
+
}]
|
|
1783
|
+
]);
|
|
1784
|
+
var Moon = icon("moon", [["path", { "d": "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" }]]);
|
|
1785
|
+
var Paintbrush = icon("paintbrush", [
|
|
1786
|
+
["path", { "d": "m14.622 17.897-10.68-2.913" }],
|
|
1787
|
+
["path", { "d": "M18.376 2.622a1 1 0 1 1 3.002 3.002L17.36 9.643a.5.5 0 0 0 0 .707l.944.944a2.41 2.41 0 0 1 0 3.408l-.944.944a.5.5 0 0 1-.707 0L8.354 7.348a.5.5 0 0 1 0-.707l.944-.944a2.41 2.41 0 0 1 3.408 0l.944.944a.5.5 0 0 0 .707 0z" }],
|
|
1788
|
+
["path", { "d": "M9 8c-1.804 2.71-3.97 3.46-6.583 3.948a.507.507 0 0 0-.302.819l7.32 8.883a1 1 0 0 0 1.185.204C12.735 20.405 16 16.792 16 15" }]
|
|
1789
|
+
]);
|
|
1790
|
+
var Palette = icon("palette", [
|
|
1791
|
+
["path", { "d": "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" }],
|
|
1792
|
+
["circle", {
|
|
1793
|
+
"cx": "13.5",
|
|
1794
|
+
"cy": "6.5",
|
|
1795
|
+
"r": ".5",
|
|
1796
|
+
"fill": "currentColor"
|
|
1797
|
+
}],
|
|
1798
|
+
["circle", {
|
|
1799
|
+
"cx": "17.5",
|
|
1800
|
+
"cy": "10.5",
|
|
1801
|
+
"r": ".5",
|
|
1802
|
+
"fill": "currentColor"
|
|
1803
|
+
}],
|
|
1804
|
+
["circle", {
|
|
1805
|
+
"cx": "6.5",
|
|
1806
|
+
"cy": "12.5",
|
|
1807
|
+
"r": ".5",
|
|
1808
|
+
"fill": "currentColor"
|
|
1809
|
+
}],
|
|
1810
|
+
["circle", {
|
|
1811
|
+
"cx": "8.5",
|
|
1812
|
+
"cy": "7.5",
|
|
1813
|
+
"r": ".5",
|
|
1814
|
+
"fill": "currentColor"
|
|
1815
|
+
}]
|
|
1816
|
+
]);
|
|
1817
|
+
var RotateCcw = icon("rotate-ccw", [["path", { "d": "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }], ["path", { "d": "M3 3v5h5" }]]);
|
|
1818
|
+
var ScanLine = icon("scan-line", [
|
|
1819
|
+
["path", { "d": "M3 7V5a2 2 0 0 1 2-2h2" }],
|
|
1820
|
+
["path", { "d": "M17 3h2a2 2 0 0 1 2 2v2" }],
|
|
1821
|
+
["path", { "d": "M21 17v2a2 2 0 0 1-2 2h-2" }],
|
|
1822
|
+
["path", { "d": "M7 21H5a2 2 0 0 1-2-2v-2" }],
|
|
1823
|
+
["path", { "d": "M7 12h10" }]
|
|
1824
|
+
]);
|
|
1825
|
+
var Settings = icon("settings", [["path", { "d": "M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" }], ["circle", {
|
|
1826
|
+
"cx": "12",
|
|
1827
|
+
"cy": "12",
|
|
1828
|
+
"r": "3"
|
|
1829
|
+
}]]);
|
|
1830
|
+
var Shuffle = icon("shuffle", [
|
|
1831
|
+
["path", { "d": "m18 14 4 4-4 4" }],
|
|
1832
|
+
["path", { "d": "m18 2 4 4-4 4" }],
|
|
1833
|
+
["path", { "d": "M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22" }],
|
|
1834
|
+
["path", { "d": "M2 6h1.972a4 4 0 0 1 3.6 2.2" }],
|
|
1835
|
+
["path", { "d": "M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45" }]
|
|
1836
|
+
]);
|
|
1837
|
+
var Sun = icon("sun", [
|
|
1838
|
+
["circle", {
|
|
1839
|
+
"cx": "12",
|
|
1840
|
+
"cy": "12",
|
|
1841
|
+
"r": "4"
|
|
1842
|
+
}],
|
|
1843
|
+
["path", { "d": "M12 2v2" }],
|
|
1844
|
+
["path", { "d": "M12 20v2" }],
|
|
1845
|
+
["path", { "d": "m4.93 4.93 1.41 1.41" }],
|
|
1846
|
+
["path", { "d": "m17.66 17.66 1.41 1.41" }],
|
|
1847
|
+
["path", { "d": "M2 12h2" }],
|
|
1848
|
+
["path", { "d": "M20 12h2" }],
|
|
1849
|
+
["path", { "d": "m6.34 17.66-1.41 1.41" }],
|
|
1850
|
+
["path", { "d": "m19.07 4.93-1.41 1.41" }]
|
|
1851
|
+
]);
|
|
1852
|
+
var Trash2 = icon("trash", [
|
|
1853
|
+
["path", { "d": "M10 11v6" }],
|
|
1854
|
+
["path", { "d": "M14 11v6" }],
|
|
1855
|
+
["path", { "d": "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }],
|
|
1856
|
+
["path", { "d": "M3 6h18" }],
|
|
1857
|
+
["path", { "d": "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }]
|
|
1858
|
+
]);
|
|
1859
|
+
var Upload = icon("upload", [
|
|
1860
|
+
["path", { "d": "M12 3v12" }],
|
|
1861
|
+
["path", { "d": "m17 8-5-5-5 5" }],
|
|
1862
|
+
["path", { "d": "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }]
|
|
1863
|
+
]);
|
|
1864
|
+
var UserRound = icon("user-round", [["circle", {
|
|
1865
|
+
"cx": "12",
|
|
1866
|
+
"cy": "8",
|
|
1867
|
+
"r": "5"
|
|
1868
|
+
}], ["path", { "d": "M20 21a8 8 0 0 0-16 0" }]]);
|
|
1869
|
+
var Wand2 = icon("wand-sparkles", [
|
|
1870
|
+
["path", { "d": "m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72" }],
|
|
1871
|
+
["path", { "d": "m14 7 3 3" }],
|
|
1872
|
+
["path", { "d": "M5 6v4" }],
|
|
1873
|
+
["path", { "d": "M19 14v4" }],
|
|
1874
|
+
["path", { "d": "M10 2v2" }],
|
|
1875
|
+
["path", { "d": "M7 8H3" }],
|
|
1876
|
+
["path", { "d": "M21 16h-4" }],
|
|
1877
|
+
["path", { "d": "M11 3H9" }]
|
|
1878
|
+
]);
|
|
1879
|
+
var X = icon("x", [["path", { "d": "M18 6 6 18" }], ["path", { "d": "m6 6 12 12" }]]);
|
|
1686
1880
|
/** Images are scaled down to at most this many pixels on their longest side before sampling. */
|
|
1687
1881
|
var SAMPLE_SIZE = 200;
|
|
1688
1882
|
/** PDF pages read, from the first. */
|
|
@@ -1698,12 +1892,16 @@ var hslOf = (hex) => rgbToHsl(hexToRgb(hex));
|
|
|
1698
1892
|
/**
|
|
1699
1893
|
* The main colours in an image or PDF, most important first.
|
|
1700
1894
|
* @param {File} file
|
|
1895
|
+
* @param {{ loadPdf?: (() => Promise<any>) | null }} [options] PDF support, from 'colorsbymax/pdf'
|
|
1701
1896
|
* @returns {Promise<{ colours: string[], from: 'image' | 'pdf-text' | 'pdf' }>}
|
|
1702
1897
|
*/
|
|
1703
|
-
async function coloursFromFile(file) {
|
|
1898
|
+
async function coloursFromFile(file, { loadPdf = null } = {}) {
|
|
1704
1899
|
if (file.size > 26214400) throw new Error("That file is over 25 MB. Try a smaller one.");
|
|
1705
|
-
if (file.type === "application/pdf" || /\.pdf$/i.test(file.name))
|
|
1706
|
-
|
|
1900
|
+
if (file.type === "application/pdf" || /\.pdf$/i.test(file.name)) {
|
|
1901
|
+
if (!loadPdf) throw new Error("PDFs aren’t supported on this site. Try an image of the palette instead.");
|
|
1902
|
+
return coloursFromPdf(file, loadPdf);
|
|
1903
|
+
}
|
|
1904
|
+
if (!file.type.startsWith("image/")) throw new Error(`Choose an image (PNG, JPG, WebP, SVG…)${loadPdf ? " or a PDF" : ""}.`);
|
|
1707
1905
|
const bitmap = await loadImage(file);
|
|
1708
1906
|
return {
|
|
1709
1907
|
colours: dominantColours([pixelsOf(bitmap, bitmap.width, bitmap.height)]),
|
|
@@ -1789,11 +1987,10 @@ function isEdgeBlend(m, kept) {
|
|
|
1789
1987
|
}
|
|
1790
1988
|
return false;
|
|
1791
1989
|
}
|
|
1792
|
-
async function coloursFromPdf(file) {
|
|
1990
|
+
async function coloursFromPdf(file, loadPdf) {
|
|
1793
1991
|
let pdfjs;
|
|
1794
1992
|
try {
|
|
1795
|
-
pdfjs = await
|
|
1796
|
-
await import("pdfjs-dist/build/pdf.worker.min.mjs");
|
|
1993
|
+
pdfjs = await loadPdf();
|
|
1797
1994
|
} catch {
|
|
1798
1995
|
throw new Error("Couldn’t load the PDF reader. Check your connection and try again.");
|
|
1799
1996
|
}
|
|
@@ -2847,7 +3044,7 @@ function PresetGrid() {
|
|
|
2847
3044
|
children: loadError ? "Couldn’t load the theme library. Check your connection and reopen the panel." : "Loading themes…"
|
|
2848
3045
|
}) : visible.length === 0 && !q && category === "yours" ? /* @__PURE__ */ jsx("p", {
|
|
2849
3046
|
className: "rounded-xl border border-dashed border-zinc-300 px-4 py-5 text-center text-xs text-zinc-600",
|
|
2850
|
-
children: "Nothing here yet. Palettes you create in Custom palettes, import, or build from
|
|
3047
|
+
children: "Nothing here yet. Palettes you create in Custom palettes, import, or build from a file in Import / export are saved here."
|
|
2851
3048
|
}) : visible.length === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
2852
3049
|
className: "py-6 text-center text-xs text-zinc-600",
|
|
2853
3050
|
role: "status",
|
|
@@ -3296,9 +3493,10 @@ var FROM_NOTE = {
|
|
|
3296
3493
|
pdf: "Picked from the colours on the first pages.",
|
|
3297
3494
|
"pdf-text": "Found colour codes written in the PDF."
|
|
3298
3495
|
};
|
|
3299
|
-
/** Builds a custom palette from the colours in an uploaded image or PDF. */
|
|
3496
|
+
/** Builds a custom palette from the colours in an uploaded image, or a PDF where the site allows it. */
|
|
3300
3497
|
function PaletteFromFile() {
|
|
3301
|
-
const { addPalette } = useTheme();
|
|
3498
|
+
const { addPalette, loadPdf } = useTheme();
|
|
3499
|
+
const kinds = loadPdf ? "image or PDF" : "image";
|
|
3302
3500
|
const savedToYours = useSavedToYours();
|
|
3303
3501
|
const [busy, setBusy] = useState(false);
|
|
3304
3502
|
const [error, setError] = useState(null);
|
|
@@ -3315,7 +3513,7 @@ function PaletteFromFile() {
|
|
|
3315
3513
|
setBusy(true);
|
|
3316
3514
|
setError(null);
|
|
3317
3515
|
try {
|
|
3318
|
-
const { colours, from } = await coloursFromFile(file);
|
|
3516
|
+
const { colours, from } = await coloursFromFile(file, { loadPdf });
|
|
3319
3517
|
if (!colours.length) throw new Error("Couldn’t find any colours in that file.");
|
|
3320
3518
|
setFound({
|
|
3321
3519
|
fileName: file.name,
|
|
@@ -3339,9 +3537,9 @@ function PaletteFromFile() {
|
|
|
3339
3537
|
return /* @__PURE__ */ jsxs("div", {
|
|
3340
3538
|
className: "space-y-1.5",
|
|
3341
3539
|
children: [
|
|
3342
|
-
/* @__PURE__ */
|
|
3540
|
+
/* @__PURE__ */ jsxs("p", {
|
|
3343
3541
|
className: "text-xs font-medium text-zinc-700",
|
|
3344
|
-
children: "Palette from an
|
|
3542
|
+
children: ["Palette from an ", kinds]
|
|
3345
3543
|
}),
|
|
3346
3544
|
/* @__PURE__ */ jsxs("div", {
|
|
3347
3545
|
onDragOver: (e) => {
|
|
@@ -3368,16 +3566,20 @@ function PaletteFromFile() {
|
|
|
3368
3566
|
}) : /* @__PURE__ */ jsx(ImageUp, {
|
|
3369
3567
|
className: "w-3.5 h-3.5",
|
|
3370
3568
|
"aria-hidden": "true"
|
|
3371
|
-
}), busy ? "Reading colours…" :
|
|
3569
|
+
}), busy ? "Reading colours…" : `Choose ${kinds}…`]
|
|
3372
3570
|
}),
|
|
3373
|
-
/* @__PURE__ */
|
|
3571
|
+
/* @__PURE__ */ jsxs("p", {
|
|
3374
3572
|
className: "mt-1.5 text-[11px] text-zinc-600",
|
|
3375
|
-
children:
|
|
3573
|
+
children: [
|
|
3574
|
+
"or drop one here. A mood board, screenshot or photo",
|
|
3575
|
+
loadPdf ? ", or a brand guide PDF," : "",
|
|
3576
|
+
" works."
|
|
3577
|
+
]
|
|
3376
3578
|
}),
|
|
3377
3579
|
/* @__PURE__ */ jsx("input", {
|
|
3378
3580
|
ref: fileRef,
|
|
3379
3581
|
type: "file",
|
|
3380
|
-
accept: "image/*,application/pdf,.pdf",
|
|
3582
|
+
accept: loadPdf ? "image/*,application/pdf,.pdf" : "image/*",
|
|
3381
3583
|
className: "sr-only",
|
|
3382
3584
|
tabIndex: -1,
|
|
3383
3585
|
"aria-hidden": "true",
|
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/pdf.js
|
|
2
|
+
/** Loads PDF.js, running it on the page so there's no separate worker file to host. */
|
|
3
|
+
async function loadPdf() {
|
|
4
|
+
const pdfjs = await import("pdfjs-dist");
|
|
5
|
+
await import("pdfjs-dist/build/pdf.worker.min.mjs");
|
|
6
|
+
return pdfjs;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { loadPdf };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "colorsbymax",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A live theme switcher for any website: site themes, 700+ palettes, for light/dark modes and WCAG contrast checks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"theme",
|
|
7
7
|
"theme-switcher",
|
|
@@ -25,19 +25,38 @@
|
|
|
25
25
|
"url": "https://github.com/MaxMuyalwa/colorsbymax/issues"
|
|
26
26
|
},
|
|
27
27
|
"type": "module",
|
|
28
|
-
"main": "dist/index.js",
|
|
29
|
-
"module": "dist/index.js",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./types/index.d.ts",
|
|
31
|
+
"typesVersions": {
|
|
32
|
+
"*": {
|
|
33
|
+
"pdf": [
|
|
34
|
+
"./types/pdf.d.ts"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
30
38
|
"exports": {
|
|
31
39
|
".": {
|
|
40
|
+
"types": "./types/index.d.ts",
|
|
32
41
|
"import": "./dist/index.js",
|
|
33
42
|
"default": "./dist/index.js"
|
|
34
43
|
},
|
|
44
|
+
"./pdf": {
|
|
45
|
+
"types": "./types/pdf.d.ts",
|
|
46
|
+
"import": "./dist/pdf.js",
|
|
47
|
+
"default": "./dist/pdf.js"
|
|
48
|
+
},
|
|
35
49
|
"./tokens.css": "./src/tokens.css",
|
|
36
50
|
"./package.json": "./package.json"
|
|
37
51
|
},
|
|
38
52
|
"sideEffects": false,
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
39
56
|
"files": [
|
|
40
57
|
"dist",
|
|
58
|
+
"types/index.d.ts",
|
|
59
|
+
"types/pdf.d.ts",
|
|
41
60
|
"src/tokens.css",
|
|
42
61
|
"LICENSE",
|
|
43
62
|
"THIRD_PARTY_NOTICES.md"
|
|
@@ -45,27 +64,36 @@
|
|
|
45
64
|
"scripts": {
|
|
46
65
|
"dev": "vite",
|
|
47
66
|
"build": "node scripts/build-css.mjs && vite build --config vite.lib.config.js",
|
|
48
|
-
"
|
|
67
|
+
"typecheck": "tsc -p types && node scripts/check-types.mjs",
|
|
68
|
+
"prepublishOnly": "npm run build && npm run typecheck",
|
|
49
69
|
"build:demo": "vite build",
|
|
50
70
|
"css": "node scripts/build-css.mjs",
|
|
71
|
+
"icons": "node scripts/build-icons.mjs",
|
|
51
72
|
"presets": "node scripts/generate-presets.mjs"
|
|
52
73
|
},
|
|
53
74
|
"peerDependencies": {
|
|
54
75
|
"react": "^18.2.0 || ^19.0.0",
|
|
55
|
-
"react-dom": "^18.2.0 || ^19.0.0"
|
|
76
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
77
|
+
"pdfjs-dist": ">=4"
|
|
56
78
|
},
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"pdfjs-dist": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
60
83
|
},
|
|
61
84
|
"devDependencies": {
|
|
62
85
|
"@tailwindcss/cli": "^4.3.3",
|
|
63
86
|
"@tailwindcss/vite": "^4.3.3",
|
|
87
|
+
"@types/react": "^19.3.0",
|
|
88
|
+
"@types/react-dom": "^19.3.0",
|
|
64
89
|
"@vitejs/plugin-react": "^6.1.1",
|
|
90
|
+
"lucide-react": "^1.48.0",
|
|
65
91
|
"nice-color-palettes": "^4.0.0",
|
|
92
|
+
"pdfjs-dist": "^6.3.289",
|
|
66
93
|
"react": "^19.3.0",
|
|
67
94
|
"react-dom": "^19.3.0",
|
|
68
95
|
"tailwindcss": "^4.3.3",
|
|
96
|
+
"typescript": "^5.9.3",
|
|
69
97
|
"vite": "^8.3.1"
|
|
70
98
|
},
|
|
71
99
|
"license": "MIT"
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Type declarations for colorsbymax. The package is written in JavaScript; these describe its
|
|
2
|
+
// public API and are checked against real usage by types/check.tsx (npm run typecheck).
|
|
3
|
+
|
|
4
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------- tokens and themes
|
|
7
|
+
|
|
8
|
+
/** Every themeable colour. Each is exposed to CSS as `--color-<key>`. */
|
|
9
|
+
export type TokenKey =
|
|
10
|
+
| 'primary' | 'primary-dark' | 'primary-alt' | 'on-primary'
|
|
11
|
+
| 'background' | 'surface' | 'secondary' | 'on-secondary' | 'accent' | 'on-accent' | 'border' | 'shadow'
|
|
12
|
+
| 'ink' | 'ink-secondary' | 'ink-muted'
|
|
13
|
+
| 'success' | 'warning' | 'danger' | 'info'
|
|
14
|
+
| 'data-1' | 'data-2' | 'data-3' | 'data-4' | 'data-5' | 'data-6' | 'data-7' | 'data-8'
|
|
15
|
+
| 'app-background' | 'app-input' | 'app-border' | 'app-primary' | 'app-shadow-dark' | 'app-shadow-light' | 'app-ink' | 'app-ink-muted'
|
|
16
|
+
|
|
17
|
+
/** Token key → `"#rrggbb"`. */
|
|
18
|
+
export type ThemeTokens = Record<TokenKey, string>
|
|
19
|
+
|
|
20
|
+
export interface Theme {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
tokens: ThemeTokens
|
|
24
|
+
/** A palette the visitor created or imported. */
|
|
25
|
+
custom?: boolean
|
|
26
|
+
/** From the generated library. */
|
|
27
|
+
library?: boolean
|
|
28
|
+
/** One of the host site's own themes (including scan suggestions). */
|
|
29
|
+
site?: boolean
|
|
30
|
+
/** Suggested by a site scan. */
|
|
31
|
+
scanned?: boolean
|
|
32
|
+
/** A generated dark twin of a light theme. */
|
|
33
|
+
derived?: boolean
|
|
34
|
+
/** Library categories, for library themes. */
|
|
35
|
+
tags?: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TokenGroup {
|
|
39
|
+
group: string
|
|
40
|
+
tokens: { key: TokenKey; label: string; usage: string }[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const TOKEN_GROUPS: TokenGroup[]
|
|
44
|
+
export const TOKEN_KEYS: TokenKey[]
|
|
45
|
+
/** The neutral default theme's tokens (Slate). */
|
|
46
|
+
export const BASE_TOKENS: ThemeTokens
|
|
47
|
+
/** The hand-tuned themes shown as "Max's picks". */
|
|
48
|
+
export const PRESETS: Theme[]
|
|
49
|
+
|
|
50
|
+
/** Fills missing tokens from `base` (the neutral default unless given), deriving the secondary-area ones. */
|
|
51
|
+
export function completeTokens(partial: Partial<ThemeTokens>, base?: ThemeTokens): ThemeTokens
|
|
52
|
+
/** Derives the secondary-area (`app-*`) tokens from a theme's main palette. */
|
|
53
|
+
export function deriveAppTokens(tokens: ThemeTokens): Pick<ThemeTokens, Extract<TokenKey, `app-${string}`>>
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------- React API
|
|
56
|
+
|
|
57
|
+
export interface ColorsByMaxConfig {
|
|
58
|
+
/** Name of the first theme group, e.g. "Tsungi". Detected from the page if left out. */
|
|
59
|
+
siteName?: string
|
|
60
|
+
/** localStorage key; must match the pre-paint script. Defaults to "colorsbymax". */
|
|
61
|
+
storageKey?: string
|
|
62
|
+
/** The site's own colours. Missing tokens are filled in. */
|
|
63
|
+
defaultTheme?: { name: string; tokens: Partial<ThemeTokens> }
|
|
64
|
+
/** Extra themes made for the site. */
|
|
65
|
+
themes?: { id: string; name: string; tokens: Partial<ThemeTokens> }[]
|
|
66
|
+
/** Where each token is used on the site, shown in the colour editors. */
|
|
67
|
+
usage?: Partial<Record<TokenKey, string>>
|
|
68
|
+
/** Colour the page's scrollbars from the theme. Default true. */
|
|
69
|
+
scrollbars?: boolean
|
|
70
|
+
/** Enables PDF uploads: pass `loadPdf` from 'colorsbymax/pdf' (needs pdfjs-dist installed). */
|
|
71
|
+
pdf?: () => Promise<unknown>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ThemeProviderProps {
|
|
75
|
+
config?: ColorsByMaxConfig
|
|
76
|
+
children?: ReactNode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Applies the saved or chosen theme to the page and provides it to the switcher and useTheme(). */
|
|
80
|
+
export function ThemeProvider(props: ThemeProviderProps): ReactElement
|
|
81
|
+
|
|
82
|
+
/** The floating colour button and panel. Render it once, inside ThemeProvider. */
|
|
83
|
+
export function ThemeSwitcher(): ReactElement | null
|
|
84
|
+
|
|
85
|
+
export interface ScanResult {
|
|
86
|
+
at: number
|
|
87
|
+
palette: string[]
|
|
88
|
+
themes: Theme[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ThemeState {
|
|
92
|
+
activeId: string
|
|
93
|
+
overrides: Partial<ThemeTokens>
|
|
94
|
+
customs: Theme[]
|
|
95
|
+
snapshot: Pick<Theme, 'id' | 'name' | 'tokens'> | null
|
|
96
|
+
scanned: ScanResult | null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ThemeApi {
|
|
100
|
+
state: ThemeState
|
|
101
|
+
storageKey: string
|
|
102
|
+
siteName: string
|
|
103
|
+
usage: Partial<Record<TokenKey, string>>
|
|
104
|
+
loadPdf: (() => Promise<unknown>) | null
|
|
105
|
+
/** The site's own themes, then any scan suggestions. */
|
|
106
|
+
siteThemes: Theme[]
|
|
107
|
+
defaultTheme: Theme
|
|
108
|
+
presets: Theme[]
|
|
109
|
+
customs: Theme[]
|
|
110
|
+
/** Site themes, presets and custom palettes (not the library). */
|
|
111
|
+
themes: Theme[]
|
|
112
|
+
/** The applied theme. */
|
|
113
|
+
active: Theme
|
|
114
|
+
/** The applied colours: the active theme plus any overrides. */
|
|
115
|
+
tokens: ThemeTokens
|
|
116
|
+
/** Contrast problems in the applied colours. */
|
|
117
|
+
issues: ContrastIssue[]
|
|
118
|
+
|
|
119
|
+
/** Selects a theme by id; pass the theme itself for library themes and dark twins. */
|
|
120
|
+
selectTheme(id: string, theme?: Theme): void
|
|
121
|
+
/** Creates a custom palette from the current colours and applies it. Returns its id. */
|
|
122
|
+
createCustom(name: string): string
|
|
123
|
+
updateCustomToken(id: string, key: TokenKey, value: string): void
|
|
124
|
+
renameCustom(id: string, name: string): void
|
|
125
|
+
deleteCustom(id: string): void
|
|
126
|
+
/** Adds tokens as a new custom palette and applies it. Returns its id. */
|
|
127
|
+
addPalette(name: string, tokens: Partial<ThemeTokens>): string
|
|
128
|
+
|
|
129
|
+
setOverride(key: TokenKey, value: string): void
|
|
130
|
+
clearOverride(key: TokenKey): void
|
|
131
|
+
clearOverrides(): void
|
|
132
|
+
resetToDefault(): void
|
|
133
|
+
|
|
134
|
+
scanned: ScanResult | null
|
|
135
|
+
/** Reads the page's colours and adds themes built around them. Resolves to counts for the UI. */
|
|
136
|
+
runScan(): Promise<{ colours: number; themes: number }>
|
|
137
|
+
clearScan(): void
|
|
138
|
+
|
|
139
|
+
/** Fixes one issue by adjusting lightness. Returns false if no single change could. */
|
|
140
|
+
fixIssue(issue: ContrastIssue): boolean
|
|
141
|
+
fixAllIssues(): void
|
|
142
|
+
|
|
143
|
+
/** The applied theme as `{ name, tokens }` JSON. */
|
|
144
|
+
exportTheme(): string
|
|
145
|
+
/** Imports `{ name, tokens }` JSON as a custom palette. Returns an error message, or null. */
|
|
146
|
+
importTheme(json: string): string | null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The theme state and actions. Must be called inside ThemeProvider. */
|
|
150
|
+
export function useTheme(): ThemeApi
|
|
151
|
+
|
|
152
|
+
/** Writes each token to `--color-<key>` on `<html>`. */
|
|
153
|
+
export function applyTokens(tokens: ThemeTokens): void
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------- pre-paint and settings
|
|
156
|
+
|
|
157
|
+
export const DEFAULT_STORAGE_KEY: 'colorsbymax'
|
|
158
|
+
/** Source of an inline `<script>` for `<head>` that applies the saved theme before first paint. */
|
|
159
|
+
export function prePaintScript(storageKey?: string): string
|
|
160
|
+
|
|
161
|
+
export interface PanelSettings {
|
|
162
|
+
mode: 'light' | 'dark' | 'system'
|
|
163
|
+
showPicks: boolean
|
|
164
|
+
showLibrary: boolean
|
|
165
|
+
showCustom: boolean
|
|
166
|
+
showOverrides: boolean
|
|
167
|
+
showImportExport: boolean
|
|
168
|
+
draggable: boolean
|
|
169
|
+
animateDot: boolean
|
|
170
|
+
panelWidth: number | null
|
|
171
|
+
panelHeight: number | 'full' | null
|
|
172
|
+
libraryCollapsed: boolean
|
|
173
|
+
}
|
|
174
|
+
/** The visitor settings the panel starts with. */
|
|
175
|
+
export const DEFAULT_SETTINGS: PanelSettings
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------- contrast
|
|
178
|
+
|
|
179
|
+
export type PairingKind = 'text' | 'large-text' | 'non-text'
|
|
180
|
+
|
|
181
|
+
export interface Pairing {
|
|
182
|
+
id: string
|
|
183
|
+
label: string
|
|
184
|
+
kind: PairingKind
|
|
185
|
+
fg(tokens: ThemeTokens): string
|
|
186
|
+
bg(tokens: ThemeTokens): string
|
|
187
|
+
/** Tokens auto-fix may adjust, in preference order. */
|
|
188
|
+
fixable: TokenKey[]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface ContrastIssue {
|
|
192
|
+
pairing: Pairing
|
|
193
|
+
ratio: number
|
|
194
|
+
required: number
|
|
195
|
+
/** e.g. "Primary text on background: 2.7:1 — text will be hard to read (needs 4.5:1)" */
|
|
196
|
+
message: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Every foreground/background pairing colorsbymax checks. */
|
|
200
|
+
export const PAIRINGS: Pairing[]
|
|
201
|
+
export const MIN_CONTRAST_TEXT: number
|
|
202
|
+
export const MIN_CONTRAST_LARGE_TEXT: number
|
|
203
|
+
export const MIN_CONTRAST_NON_TEXT: number
|
|
204
|
+
export const MIN_RAMP_STEP_DELTA_E: number
|
|
205
|
+
|
|
206
|
+
/** Every failing pairing in a theme. */
|
|
207
|
+
export function checkTheme(tokens: ThemeTokens): ContrastIssue[]
|
|
208
|
+
/** The smallest lightness change to one token that makes a pairing pass, or null. */
|
|
209
|
+
export function suggestFix(pairing: Pairing, tokens: ThemeTokens): { key: TokenKey; value: string } | null
|
|
210
|
+
/** Fixes failing pairings until the theme passes or nothing more helps. Returns the changed tokens. */
|
|
211
|
+
export function fixAll(tokens: ThemeTokens): Partial<ThemeTokens>
|
|
212
|
+
/** Problems with an ordered colour ramp drawn on `surface`, as sentences. */
|
|
213
|
+
export function checkRamp(colors: string[], surface: string): string[]
|
|
214
|
+
|
|
215
|
+
// ---------------------------------------------------------------- colour helpers
|
|
216
|
+
|
|
217
|
+
/** WCAG 2 contrast ratio between two hex colours, 1–21. */
|
|
218
|
+
export function contrastRatio(a: string, b: string): number
|
|
219
|
+
/** "#abc", "abc" or "#AABBCC" → "#aabbcc"; null for anything else. */
|
|
220
|
+
export function normalizeHex(input: unknown): string | null
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------- light and dark
|
|
223
|
+
|
|
224
|
+
/** True when a theme's page background is dark. */
|
|
225
|
+
export function isDarkTheme(tokens: ThemeTokens): boolean
|
|
226
|
+
/** Dark versions of a light theme's tokens, adjusted to pass contrast. */
|
|
227
|
+
export function darkTokens(tokens: ThemeTokens): ThemeTokens
|
|
228
|
+
/** The dark twin of a light theme (themes that are already dark come back unchanged). */
|
|
229
|
+
export function toDark<T extends Theme>(theme: T): T
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------- site scan
|
|
232
|
+
|
|
233
|
+
export interface ColourTally {
|
|
234
|
+
hex: string
|
|
235
|
+
/** Area painted as a background. */
|
|
236
|
+
bg: number
|
|
237
|
+
/** Weighted amount of text in this colour. */
|
|
238
|
+
text: number
|
|
239
|
+
/** Border length in this colour. */
|
|
240
|
+
border: number
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type Roles = Partial<Record<TokenKey, string>>
|
|
244
|
+
|
|
245
|
+
/** The page's painted colours, ignoring any applied theme. `exclude` is a selector to skip. */
|
|
246
|
+
export function collectColors(options?: { exclude?: string }): ColourTally[]
|
|
247
|
+
/** Works out token roles from collected colours. */
|
|
248
|
+
export function inferRoles(colors: ColourTally[]): { roles: Roles; palette: string[] }
|
|
249
|
+
/** A complete theme from detected roles, deriving whatever wasn't found. */
|
|
250
|
+
export function themeFromRoles(
|
|
251
|
+
roles: Roles,
|
|
252
|
+
variant?: { softness?: number; boldness?: number; complementary?: boolean },
|
|
253
|
+
): ThemeTokens
|
|
254
|
+
/** Themes suggested for a scanned site, named after it, plus the closest library themes. */
|
|
255
|
+
export function suggestThemes(scan: { roles: Roles }, siteName: string, library?: Theme[]): Theme[]
|
|
256
|
+
/** Best guess at the site's name: og:site_name, then the title, then the host. */
|
|
257
|
+
export function detectSiteName(doc?: Document): string
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------- palettes from files
|
|
260
|
+
|
|
261
|
+
/** The main colours in an image (or a PDF, with `loadPdf`), most important first. */
|
|
262
|
+
export function coloursFromFile(
|
|
263
|
+
file: File,
|
|
264
|
+
options?: { loadPdf?: (() => Promise<unknown>) | null },
|
|
265
|
+
): Promise<{ colours: string[]; from: 'image' | 'pdf-text' | 'pdf' }>
|
|
266
|
+
/** The dominant colours in sets of RGBA pixels, edge blends removed. */
|
|
267
|
+
export function dominantColours(pixelSets: Uint8ClampedArray[]): string[]
|
|
268
|
+
/** Assigns a palette's colours to theme roles. */
|
|
269
|
+
export function rolesFromPalette(colours: string[]): Roles
|
|
270
|
+
/** A complete, contrast-checked theme built around a palette. */
|
|
271
|
+
export function themeFromPalette(colours: string[]): ThemeTokens
|
package/types/pdf.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Type declarations for 'colorsbymax/pdf'.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loads PDF.js for PDF uploads. Pass it as `pdf` in the ThemeProvider config; needs the
|
|
5
|
+
* pdfjs-dist package installed. PDF.js downloads only when a visitor picks a PDF.
|
|
6
|
+
*/
|
|
7
|
+
export function loadPdf(): Promise<unknown>
|