colorsbymax 0.1.0 → 0.2.0

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/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.0",
4
- "description": "colorsbymax™ by mrmaxdesigns: a runtime theme switcher with presets, a 715-theme library, custom palettes and WCAG contrast checks.",
3
+ "version": "0.2.0",
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,47 +25,87 @@
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
+ "auto": [
37
+ "./types/auto.d.ts"
38
+ ]
39
+ }
40
+ },
30
41
  "exports": {
31
42
  ".": {
43
+ "types": "./types/index.d.ts",
32
44
  "import": "./dist/index.js",
33
45
  "default": "./dist/index.js"
34
46
  },
47
+ "./auto": {
48
+ "types": "./types/auto.d.ts",
49
+ "import": "./dist/auto.js",
50
+ "default": "./dist/auto.js"
51
+ },
52
+ "./pdf": {
53
+ "types": "./types/pdf.d.ts",
54
+ "import": "./dist/pdf.js",
55
+ "default": "./dist/pdf.js"
56
+ },
35
57
  "./tokens.css": "./src/tokens.css",
36
58
  "./package.json": "./package.json"
37
59
  },
38
- "sideEffects": false,
60
+ "sideEffects": [
61
+ "./dist/auto.js"
62
+ ],
63
+ "engines": {
64
+ "node": ">=18"
65
+ },
39
66
  "files": [
40
67
  "dist",
68
+ "types/index.d.ts",
69
+ "types/auto.d.ts",
70
+ "types/pdf.d.ts",
41
71
  "src/tokens.css",
42
72
  "LICENSE",
43
- "THIRD_PARTY_NOTICES.md"
73
+ "THIRD_PARTY_NOTICES.md",
74
+ "CHANGELOG.md"
44
75
  ],
45
76
  "scripts": {
46
77
  "dev": "vite",
47
78
  "build": "node scripts/build-css.mjs && vite build --config vite.lib.config.js",
48
- "prepare": "vite build --config vite.lib.config.js",
79
+ "typecheck": "tsc -p types && node scripts/check-types.mjs",
80
+ "prepublishOnly": "npm run build && npm run typecheck",
49
81
  "build:demo": "vite build",
50
82
  "css": "node scripts/build-css.mjs",
83
+ "icons": "node scripts/build-icons.mjs",
51
84
  "presets": "node scripts/generate-presets.mjs"
52
85
  },
53
86
  "peerDependencies": {
54
87
  "react": "^18.2.0 || ^19.0.0",
55
- "react-dom": "^18.2.0 || ^19.0.0"
88
+ "react-dom": "^18.2.0 || ^19.0.0",
89
+ "pdfjs-dist": ">=4"
56
90
  },
57
- "dependencies": {
58
- "lucide-react": "^1.48.0",
59
- "pdfjs-dist": "^6.3.289"
91
+ "peerDependenciesMeta": {
92
+ "pdfjs-dist": {
93
+ "optional": true
94
+ }
60
95
  },
61
96
  "devDependencies": {
62
97
  "@tailwindcss/cli": "^4.3.3",
63
98
  "@tailwindcss/vite": "^4.3.3",
99
+ "@types/react": "^19.3.0",
100
+ "@types/react-dom": "^19.3.0",
64
101
  "@vitejs/plugin-react": "^6.1.1",
102
+ "lucide-react": "^1.48.0",
65
103
  "nice-color-palettes": "^4.0.0",
104
+ "pdfjs-dist": "^6.3.289",
66
105
  "react": "^19.3.0",
67
106
  "react-dom": "^19.3.0",
68
107
  "tailwindcss": "^4.3.3",
108
+ "typescript": "^5.9.3",
69
109
  "vite": "^8.3.1"
70
110
  },
71
111
  "license": "MIT"
@@ -0,0 +1,10 @@
1
+ // Type declarations for 'colorsbymax/auto'. Importing the module mounts the colour button by
2
+ // itself once the page has rendered: `import 'colorsbymax/auto'`.
3
+
4
+ import type { ColorsByMaxConfig } from './index.js'
5
+
6
+ /**
7
+ * Mounts the colour button and panel with settings, instead of the automatic mount. Calling it
8
+ * again replaces the previous mount. Returns a function that removes it.
9
+ */
10
+ export function autoMount(config?: ColorsByMaxConfig): () => void
@@ -0,0 +1,285 @@
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
+ * Re-colour a site that doesn't paint with the --color-* variables, by swapping the colours
74
+ * actually on the page. 'auto' (the default) does it only when the site doesn't define
75
+ * --color-primary itself; false never does.
76
+ */
77
+ recolour?: boolean | 'auto'
78
+ /**
79
+ * Where the colour button starts. Default 'bottom-right', like chat and help widgets;
80
+ * 'top-right' sits just under a floating nav bar. Visitors can still drag it anywhere.
81
+ */
82
+ position?: 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
83
+ }
84
+
85
+ export interface ThemeProviderProps {
86
+ config?: ColorsByMaxConfig
87
+ children?: ReactNode
88
+ }
89
+
90
+ /** Applies the saved or chosen theme to the page and provides it to the switcher and useTheme(). */
91
+ export function ThemeProvider(props: ThemeProviderProps): ReactElement
92
+
93
+ /** The floating colour button and panel. Render it once, inside ThemeProvider. */
94
+ export function ThemeSwitcher(): ReactElement | null
95
+
96
+ export interface ScanResult {
97
+ at: number
98
+ palette: string[]
99
+ themes: Theme[]
100
+ }
101
+
102
+ export interface ThemeState {
103
+ activeId: string
104
+ overrides: Partial<ThemeTokens>
105
+ customs: Theme[]
106
+ snapshot: Pick<Theme, 'id' | 'name' | 'tokens'> | null
107
+ scanned: ScanResult | null
108
+ }
109
+
110
+ export interface ThemeApi {
111
+ state: ThemeState
112
+ storageKey: string
113
+ siteName: string
114
+ usage: Partial<Record<TokenKey, string>>
115
+ loadPdf: (() => Promise<unknown>) | null
116
+ position: 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
117
+ /** True when colorsbymax is swapping the page's own colours (the site isn't using the variables). */
118
+ recolouring: boolean
119
+ /** The site's own themes, then any scan suggestions. */
120
+ siteThemes: Theme[]
121
+ defaultTheme: Theme
122
+ presets: Theme[]
123
+ customs: Theme[]
124
+ /** Site themes, presets and custom palettes (not the library). */
125
+ themes: Theme[]
126
+ /** The applied theme. */
127
+ active: Theme
128
+ /** The applied colours: the active theme plus any overrides. */
129
+ tokens: ThemeTokens
130
+ /** Contrast problems in the applied colours. */
131
+ issues: ContrastIssue[]
132
+
133
+ /** Selects a theme by id; pass the theme itself for library themes and dark twins. */
134
+ selectTheme(id: string, theme?: Theme): void
135
+ /** Creates a custom palette from the current colours and applies it. Returns its id. */
136
+ createCustom(name: string): string
137
+ updateCustomToken(id: string, key: TokenKey, value: string): void
138
+ renameCustom(id: string, name: string): void
139
+ deleteCustom(id: string): void
140
+ /** Adds tokens as a new custom palette and applies it. Returns its id. */
141
+ addPalette(name: string, tokens: Partial<ThemeTokens>): string
142
+
143
+ setOverride(key: TokenKey, value: string): void
144
+ clearOverride(key: TokenKey): void
145
+ clearOverrides(): void
146
+ resetToDefault(): void
147
+
148
+ scanned: ScanResult | null
149
+ /** Reads the page's colours and adds themes built around them. Resolves to counts for the UI. */
150
+ runScan(): Promise<{ colours: number; themes: number }>
151
+ clearScan(): void
152
+
153
+ /** Fixes one issue by adjusting lightness. Returns false if no single change could. */
154
+ fixIssue(issue: ContrastIssue): boolean
155
+ fixAllIssues(): void
156
+
157
+ /** The applied theme as `{ name, tokens }` JSON. */
158
+ exportTheme(): string
159
+ /** Imports `{ name, tokens }` JSON as a custom palette. Returns an error message, or null. */
160
+ importTheme(json: string): string | null
161
+ }
162
+
163
+ /** The theme state and actions. Must be called inside ThemeProvider. */
164
+ export function useTheme(): ThemeApi
165
+
166
+ /** Writes each token to `--color-<key>` on `<html>`. */
167
+ export function applyTokens(tokens: ThemeTokens): void
168
+
169
+ // ---------------------------------------------------------------- pre-paint and settings
170
+
171
+ export const DEFAULT_STORAGE_KEY: 'colorsbymax'
172
+ /** Source of an inline `<script>` for `<head>` that applies the saved theme before first paint. */
173
+ export function prePaintScript(storageKey?: string): string
174
+
175
+ export interface PanelSettings {
176
+ mode: 'light' | 'dark' | 'system'
177
+ showPicks: boolean
178
+ showLibrary: boolean
179
+ showCustom: boolean
180
+ showOverrides: boolean
181
+ showImportExport: boolean
182
+ draggable: boolean
183
+ animateDot: boolean
184
+ panelWidth: number | null
185
+ panelHeight: number | 'full' | null
186
+ libraryCollapsed: boolean
187
+ }
188
+ /** The visitor settings the panel starts with. */
189
+ export const DEFAULT_SETTINGS: PanelSettings
190
+
191
+ // ---------------------------------------------------------------- contrast
192
+
193
+ export type PairingKind = 'text' | 'large-text' | 'non-text'
194
+
195
+ export interface Pairing {
196
+ id: string
197
+ label: string
198
+ kind: PairingKind
199
+ fg(tokens: ThemeTokens): string
200
+ bg(tokens: ThemeTokens): string
201
+ /** Tokens auto-fix may adjust, in preference order. */
202
+ fixable: TokenKey[]
203
+ }
204
+
205
+ export interface ContrastIssue {
206
+ pairing: Pairing
207
+ ratio: number
208
+ required: number
209
+ /** e.g. "Primary text on background: 2.7:1 — text will be hard to read (needs 4.5:1)" */
210
+ message: string
211
+ }
212
+
213
+ /** Every foreground/background pairing colorsbymax checks. */
214
+ export const PAIRINGS: Pairing[]
215
+ export const MIN_CONTRAST_TEXT: number
216
+ export const MIN_CONTRAST_LARGE_TEXT: number
217
+ export const MIN_CONTRAST_NON_TEXT: number
218
+ export const MIN_RAMP_STEP_DELTA_E: number
219
+
220
+ /** Every failing pairing in a theme. */
221
+ export function checkTheme(tokens: ThemeTokens): ContrastIssue[]
222
+ /** The smallest lightness change to one token that makes a pairing pass, or null. */
223
+ export function suggestFix(pairing: Pairing, tokens: ThemeTokens): { key: TokenKey; value: string } | null
224
+ /** Fixes failing pairings until the theme passes or nothing more helps. Returns the changed tokens. */
225
+ export function fixAll(tokens: ThemeTokens): Partial<ThemeTokens>
226
+ /** Problems with an ordered colour ramp drawn on `surface`, as sentences. */
227
+ export function checkRamp(colors: string[], surface: string): string[]
228
+
229
+ // ---------------------------------------------------------------- colour helpers
230
+
231
+ /** WCAG 2 contrast ratio between two hex colours, 1–21. */
232
+ export function contrastRatio(a: string, b: string): number
233
+ /** "#abc", "abc" or "#AABBCC" → "#aabbcc"; null for anything else. */
234
+ export function normalizeHex(input: unknown): string | null
235
+
236
+ // ---------------------------------------------------------------- light and dark
237
+
238
+ /** True when a theme's page background is dark. */
239
+ export function isDarkTheme(tokens: ThemeTokens): boolean
240
+ /** Dark versions of a light theme's tokens, adjusted to pass contrast. */
241
+ export function darkTokens(tokens: ThemeTokens): ThemeTokens
242
+ /** The dark twin of a light theme (themes that are already dark come back unchanged). */
243
+ export function toDark<T extends Theme>(theme: T): T
244
+
245
+ // ---------------------------------------------------------------- site scan
246
+
247
+ export interface ColourTally {
248
+ hex: string
249
+ /** Area painted as a background. */
250
+ bg: number
251
+ /** Weighted amount of text in this colour. */
252
+ text: number
253
+ /** Border length in this colour. */
254
+ border: number
255
+ }
256
+
257
+ export type Roles = Partial<Record<TokenKey, string>>
258
+
259
+ /** The page's painted colours, ignoring any applied theme. `exclude` is a selector to skip. */
260
+ export function collectColors(options?: { exclude?: string }): ColourTally[]
261
+ /** Works out token roles from collected colours. */
262
+ export function inferRoles(colors: ColourTally[]): { roles: Roles; palette: string[] }
263
+ /** A complete theme from detected roles, deriving whatever wasn't found. */
264
+ export function themeFromRoles(
265
+ roles: Roles,
266
+ variant?: { softness?: number; boldness?: number; complementary?: boolean },
267
+ ): ThemeTokens
268
+ /** Themes suggested for a scanned site, named after it, plus the closest library themes. */
269
+ export function suggestThemes(scan: { roles: Roles }, siteName: string, library?: Theme[]): Theme[]
270
+ /** Best guess at the site's name: og:site_name, then the title, then the host. */
271
+ export function detectSiteName(doc?: Document): string
272
+
273
+ // ---------------------------------------------------------------- palettes from files
274
+
275
+ /** The main colours in an image (or a PDF, with `loadPdf`), most important first. */
276
+ export function coloursFromFile(
277
+ file: File,
278
+ options?: { loadPdf?: (() => Promise<unknown>) | null },
279
+ ): Promise<{ colours: string[]; from: 'image' | 'pdf-text' | 'pdf' }>
280
+ /** The dominant colours in sets of RGBA pixels, edge blends removed. */
281
+ export function dominantColours(pixelSets: Uint8ClampedArray[]): string[]
282
+ /** Assigns a palette's colours to theme roles. */
283
+ export function rolesFromPalette(colours: string[]): Roles
284
+ /** A complete, contrast-checked theme built around a palette. */
285
+ 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>