colorsbymax 0.3.2 → 0.4.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/types/index.d.ts CHANGED
@@ -1,321 +1,337 @@
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
- * Hide the colour button and panel; the theme still applies. `hidden: import.meta.env.PROD`
85
- * keeps it out of production once the colours are chosen, while it still shows in development.
86
- */
87
- hidden?: boolean
88
- /**
89
- * Bring the colour button in with a short pop and a burst of the theme's colours, a moment after
90
- * the page loads (default true). With reduced motion it fades in instead.
91
- */
92
- intro?: boolean
93
- /**
94
- * Whether themes colour the site's logo for a first-time visitor (default false: the logo keeps
95
- * its own colours). Visitors can still change it in the panel's settings.
96
- */
97
- colourLogo?: boolean
98
- /**
99
- * The mode a first-time visitor starts in (default 'light'; 'system' follows their device). Any
100
- * site can start dark: every theme has a dark twin. Visitors can still change it.
101
- */
102
- defaultMode?: 'light' | 'dark' | 'system'
103
- }
104
-
105
- export interface ThemeProviderProps {
106
- config?: ColorsByMaxConfig
107
- children?: ReactNode
108
- }
109
-
110
- /** Applies the saved or chosen theme to the page and provides it to the switcher and useTheme(). */
111
- export function ThemeProvider(props: ThemeProviderProps): ReactElement
112
-
113
- /** The floating colour button and panel. Render it once, inside ThemeProvider. */
114
- export function ThemeSwitcher(): ReactElement | null
115
-
116
- export interface ScanResult {
117
- at: number
118
- palette: string[]
119
- themes: Theme[]
120
- }
121
-
122
- export interface ThemeState {
123
- activeId: string
124
- overrides: Partial<ThemeTokens>
125
- customs: Theme[]
126
- snapshot: Pick<Theme, 'id' | 'name' | 'tokens'> | null
127
- scanned: ScanResult | null
128
- }
129
-
130
- export interface ThemeApi {
131
- state: ThemeState
132
- storageKey: string
133
- siteName: string
134
- usage: Partial<Record<TokenKey, string>>
135
- loadPdf: (() => Promise<unknown>) | null
136
- position: 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
137
- /** True when the config hides the switcher; the theme still applies. */
138
- hidden: boolean
139
- /** Whether the colour button makes an entrance when it first appears. */
140
- intro: boolean
141
- /** The mode in effect: every theme shows its light or dark version. */
142
- mode: 'light' | 'dark'
143
- /** The visitor's choice; 'system' follows the device. */
144
- modeSetting: 'light' | 'dark' | 'system'
145
- /** Sets the mode, for the whole site, and remembers it. */
146
- setMode(mode: 'light' | 'dark' | 'system'): void
147
- /** True when colorsbymax is swapping the page's own colours (the site isn't using the variables). */
148
- recolouring: boolean
149
- /** Turns re-colouring of the site's logo on or off. */
150
- setLogoColouring(on: boolean): void
151
- /** The site's own themes, then any scan suggestions. */
152
- siteThemes: Theme[]
153
- defaultTheme: Theme
154
- presets: Theme[]
155
- customs: Theme[]
156
- /** Site themes, presets and custom palettes (not the library). */
157
- themes: Theme[]
158
- /** The applied theme. */
159
- active: Theme
160
- /** The applied colours: the active theme plus any overrides. */
161
- tokens: ThemeTokens
162
- /** Contrast problems in the applied colours. */
163
- issues: ContrastIssue[]
164
-
165
- /** Selects a theme by id; pass the theme itself for library themes and dark twins. */
166
- selectTheme(id: string, theme?: Theme): void
167
- /** Creates a custom palette from the current colours and applies it. Returns its id. */
168
- createCustom(name: string): string
169
- updateCustomToken(id: string, key: TokenKey, value: string): void
170
- renameCustom(id: string, name: string): void
171
- deleteCustom(id: string): void
172
- /** Adds tokens as a new custom palette and applies it. Returns its id. */
173
- addPalette(name: string, tokens: Partial<ThemeTokens>): string
174
-
175
- setOverride(key: TokenKey, value: string): void
176
- clearOverride(key: TokenKey): void
177
- clearOverrides(): void
178
- resetToDefault(): void
179
-
180
- scanned: ScanResult | null
181
- /** Reads the page's colours and adds themes built around them. Resolves to counts for the UI. */
182
- runScan(): Promise<{ colours: number; themes: number }>
183
- clearScan(): void
184
-
185
- /** Fixes one issue by adjusting lightness. Returns false if no single change could. */
186
- fixIssue(issue: ContrastIssue): boolean
187
- fixAllIssues(): void
188
-
189
- /** The applied theme as `{ name, tokens }` JSON. */
190
- exportTheme(): string
191
- /** Imports `{ name, tokens }` JSON as a custom palette. Returns an error message, or null. */
192
- importTheme(json: string): string | null
193
- }
194
-
195
- /** The theme state and actions. Must be called inside ThemeProvider. */
196
- export function useTheme(): ThemeApi
197
-
198
- /** Writes each token to `--color-<key>` on `<html>`. */
199
- export function applyTokens(tokens: ThemeTokens): void
200
-
201
- // ---------------------------------------------------------------- pre-paint and settings
202
-
203
- export const DEFAULT_STORAGE_KEY: 'colorsbymax'
204
- /** Source of an inline `<script>` for `<head>` that applies the saved theme before first paint. */
205
- export function prePaintScript(storageKey?: string): string
206
-
207
- export interface PanelSettings {
208
- mode: 'light' | 'dark' | 'system'
209
- showPicks: boolean
210
- showLibrary: boolean
211
- showCustom: boolean
212
- showOverrides: boolean
213
- showImportExport: boolean
214
- draggable: boolean
215
- animateDot: boolean
216
- panelWidth: number | null
217
- panelHeight: number | 'full' | null
218
- libraryCollapsed: boolean
219
- /** Whether themes re-colour the site's logo too. Off (the default) keeps its own colours. */
220
- colourLogo: boolean
221
- /** Hidden on this device (from the finish screen); Alt+Shift+C or ?colorsbymax brings it back. */
222
- hideButton: boolean
223
- }
224
- /** The visitor settings the panel starts with. */
225
- export const DEFAULT_SETTINGS: PanelSettings
226
-
227
- // ---------------------------------------------------------------- contrast
228
-
229
- export type PairingKind = 'text' | 'large-text' | 'non-text'
230
-
231
- export interface Pairing {
232
- id: string
233
- label: string
234
- kind: PairingKind
235
- fg(tokens: ThemeTokens): string
236
- bg(tokens: ThemeTokens): string
237
- /** Tokens auto-fix may adjust, in preference order. */
238
- fixable: TokenKey[]
239
- }
240
-
241
- export interface ContrastIssue {
242
- pairing: Pairing
243
- ratio: number
244
- required: number
245
- /** e.g. "Primary text on background: 2.7:1 — text will be hard to read (needs 4.5:1)" */
246
- message: string
247
- }
248
-
249
- /** Every foreground/background pairing colorsbymax checks. */
250
- export const PAIRINGS: Pairing[]
251
- export const MIN_CONTRAST_TEXT: number
252
- export const MIN_CONTRAST_LARGE_TEXT: number
253
- export const MIN_CONTRAST_NON_TEXT: number
254
- export const MIN_RAMP_STEP_DELTA_E: number
255
-
256
- /** Every failing pairing in a theme. */
257
- export function checkTheme(tokens: ThemeTokens): ContrastIssue[]
258
- /** The smallest lightness change to one token that makes a pairing pass, or null. */
259
- export function suggestFix(pairing: Pairing, tokens: ThemeTokens): { key: TokenKey; value: string } | null
260
- /** Fixes failing pairings until the theme passes or nothing more helps. Returns the changed tokens. */
261
- export function fixAll(tokens: ThemeTokens): Partial<ThemeTokens>
262
- /** Problems with an ordered colour ramp drawn on `surface`, as sentences. */
263
- export function checkRamp(colors: string[], surface: string): string[]
264
-
265
- // ---------------------------------------------------------------- colour helpers
266
-
267
- /** WCAG 2 contrast ratio between two hex colours, 1–21. */
268
- export function contrastRatio(a: string, b: string): number
269
- /** "#abc", "abc" or "#AABBCC" → "#aabbcc"; null for anything else. */
270
- export function normalizeHex(input: unknown): string | null
271
-
272
- // ---------------------------------------------------------------- light and dark
273
-
274
- /** True when a theme's page background is dark. */
275
- export function isDarkTheme(tokens: ThemeTokens): boolean
276
- /** Dark versions of a light theme's tokens, adjusted to pass contrast. */
277
- export function darkTokens(tokens: ThemeTokens): ThemeTokens
278
- /** The dark twin of a light theme (themes that are already dark come back unchanged). */
279
- export function toDark<T extends Theme>(theme: T): T
280
-
281
- // ---------------------------------------------------------------- site scan
282
-
283
- export interface ColourTally {
284
- hex: string
285
- /** Area painted as a background. */
286
- bg: number
287
- /** Weighted amount of text in this colour. */
288
- text: number
289
- /** Border length in this colour. */
290
- border: number
291
- }
292
-
293
- export type Roles = Partial<Record<TokenKey, string>>
294
-
295
- /** The page's painted colours, ignoring any applied theme. `exclude` is a selector to skip. */
296
- export function collectColors(options?: { exclude?: string }): ColourTally[]
297
- /** Works out token roles from collected colours. */
298
- export function inferRoles(colors: ColourTally[]): { roles: Roles; palette: string[] }
299
- /** A complete theme from detected roles, deriving whatever wasn't found. */
300
- export function themeFromRoles(
301
- roles: Roles,
302
- variant?: { softness?: number; boldness?: number; complementary?: boolean },
303
- ): ThemeTokens
304
- /** Themes suggested for a scanned site, named after it, plus the closest library themes. */
305
- export function suggestThemes(scan: { roles: Roles }, siteName: string, library?: Theme[]): Theme[]
306
- /** Best guess at the site's name: og:site_name, then the title, then the host. */
307
- export function detectSiteName(doc?: Document): string
308
-
309
- // ---------------------------------------------------------------- palettes from files
310
-
311
- /** The main colours in an image (or a PDF, with `loadPdf`), most important first. */
312
- export function coloursFromFile(
313
- file: File,
314
- options?: { loadPdf?: (() => Promise<unknown>) | null },
315
- ): Promise<{ colours: string[]; from: 'image' | 'pdf-text' | 'pdf' }>
316
- /** The dominant colours in sets of RGBA pixels, edge blends removed. */
317
- export function dominantColours(pixelSets: Uint8ClampedArray[]): string[]
318
- /** Assigns a palette's colours to theme roles. */
319
- export function rolesFromPalette(colours: string[]): Roles
320
- /** A complete, contrast-checked theme built around a palette. */
321
- export function themeFromPalette(colours: string[]): ThemeTokens
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
+ * Hide the colour button and panel; the theme still applies. `hidden: import.meta.env.PROD`
85
+ * keeps it out of production once the colours are chosen, while it still shows in development.
86
+ */
87
+ hidden?: boolean
88
+ /**
89
+ * Bring the colour button in with a short pop and a burst of the theme's colours, a moment after
90
+ * the page loads (default true). With reduced motion it fades in instead.
91
+ */
92
+ intro?: boolean
93
+ /**
94
+ * Whether themes colour the site's logo for a first-time visitor (default false: the logo keeps
95
+ * its own colours). Visitors can still change it in the panel's settings.
96
+ */
97
+ colourLogo?: boolean
98
+ /**
99
+ * How boldly a re-coloured site (one that doesn't paint with the --color-* variables) takes a
100
+ * theme, for a first-time visitor. 'colourful' (default) paints the page's parts by role, as a
101
+ * designer would: header, hero, alternating sections, cards, buttons, links, headings and footer.
102
+ * 'subtle' only swaps the colours the site already has. Visitors can switch in the panel.
103
+ */
104
+ colourStyle?: 'colourful' | 'subtle'
105
+ /**
106
+ * Parts of the panel to switch off for everyone, e.g. `{ scan: false, audit: false }`. All are on
107
+ * by default. Unlike the rest of the config it's read live, so a site can change it after loading.
108
+ */
109
+ features?: Partial<Record<'picks' | 'library' | 'search' | 'surprise' | 'scan' | 'custom' | 'overrides' | 'importExport' | 'audit' | 'addToSite' | 'colourStyle' | 'colourCount', boolean>>
110
+ /**
111
+ * The mode a first-time visitor starts in (default 'light'; 'system' follows their device). Any
112
+ * site can start dark: every theme has a dark twin. Visitors can still change it.
113
+ */
114
+ defaultMode?: 'light' | 'dark' | 'system'
115
+ }
116
+
117
+ export interface ThemeProviderProps {
118
+ config?: ColorsByMaxConfig
119
+ children?: ReactNode
120
+ }
121
+
122
+ /** Applies the saved or chosen theme to the page and provides it to the switcher and useTheme(). */
123
+ export function ThemeProvider(props: ThemeProviderProps): ReactElement
124
+
125
+ /** The floating colour button and panel. Render it once, inside ThemeProvider. */
126
+ export function ThemeSwitcher(): ReactElement | null
127
+
128
+ export interface ScanResult {
129
+ at: number
130
+ palette: string[]
131
+ themes: Theme[]
132
+ }
133
+
134
+ export interface ThemeState {
135
+ activeId: string
136
+ overrides: Partial<ThemeTokens>
137
+ customs: Theme[]
138
+ snapshot: Pick<Theme, 'id' | 'name' | 'tokens'> | null
139
+ scanned: ScanResult | null
140
+ }
141
+
142
+ export interface ThemeApi {
143
+ state: ThemeState
144
+ storageKey: string
145
+ siteName: string
146
+ usage: Partial<Record<TokenKey, string>>
147
+ loadPdf: (() => Promise<unknown>) | null
148
+ position: 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
149
+ /** True when the config hides the switcher; the theme still applies. */
150
+ hidden: boolean
151
+ /** Whether the colour button makes an entrance when it first appears. */
152
+ intro: boolean
153
+ /** The mode in effect: every theme shows its light or dark version. */
154
+ mode: 'light' | 'dark'
155
+ /** The visitor's choice; 'system' follows the device. */
156
+ modeSetting: 'light' | 'dark' | 'system'
157
+ /** Sets the mode, for the whole site, and remembers it. */
158
+ setMode(mode: 'light' | 'dark' | 'system'): void
159
+ /** True when colorsbymax is swapping the page's own colours (the site isn't using the variables). */
160
+ recolouring: boolean
161
+ /** Turns re-colouring of the site's logo on or off. */
162
+ setLogoColouring(on: boolean): void
163
+ /** How boldly a re-coloured site takes the theme (the panel's Subtle / Colourful switch). */
164
+ setColourStyle(style: 'colourful' | 'subtle'): void
165
+ /** The site's own themes, then any scan suggestions. */
166
+ siteThemes: Theme[]
167
+ defaultTheme: Theme
168
+ presets: Theme[]
169
+ customs: Theme[]
170
+ /** Site themes, presets and custom palettes (not the library). */
171
+ themes: Theme[]
172
+ /** The applied theme. */
173
+ active: Theme
174
+ /** The applied colours: the active theme plus any overrides. */
175
+ tokens: ThemeTokens
176
+ /** Contrast problems in the applied colours. */
177
+ issues: ContrastIssue[]
178
+
179
+ /** Selects a theme by id; pass the theme itself for library themes and dark twins. */
180
+ selectTheme(id: string, theme?: Theme): void
181
+ /** Creates a custom palette from the current colours and applies it. Returns its id. */
182
+ createCustom(name: string): string
183
+ updateCustomToken(id: string, key: TokenKey, value: string): void
184
+ renameCustom(id: string, name: string): void
185
+ deleteCustom(id: string): void
186
+ /** Adds tokens as a new custom palette and applies it. Returns its id. */
187
+ addPalette(name: string, tokens: Partial<ThemeTokens>): string
188
+
189
+ setOverride(key: TokenKey, value: string): void
190
+ clearOverride(key: TokenKey): void
191
+ clearOverrides(): void
192
+ resetToDefault(): void
193
+
194
+ scanned: ScanResult | null
195
+ /** Reads the page's colours and adds themes built around them. Resolves to counts for the UI. */
196
+ runScan(): Promise<{ colours: number; themes: number }>
197
+ clearScan(): void
198
+
199
+ /** Fixes one issue by adjusting lightness. Returns false if no single change could. */
200
+ fixIssue(issue: ContrastIssue): boolean
201
+ fixAllIssues(): void
202
+
203
+ /** The applied theme as `{ name, tokens }` JSON. */
204
+ exportTheme(): string
205
+ /** Imports `{ name, tokens }` JSON as a custom palette. Returns an error message, or null. */
206
+ importTheme(json: string): string | null
207
+ }
208
+
209
+ /** The theme state and actions. Must be called inside ThemeProvider. */
210
+ export function useTheme(): ThemeApi
211
+
212
+ /** Writes each token to `--color-<key>` on `<html>`. */
213
+ export function applyTokens(tokens: ThemeTokens): void
214
+
215
+ // ---------------------------------------------------------------- pre-paint and settings
216
+
217
+ export const DEFAULT_STORAGE_KEY: 'colorsbymax'
218
+ /** Source of an inline `<script>` for `<head>` that applies the saved theme before first paint. */
219
+ export function prePaintScript(storageKey?: string): string
220
+
221
+ export interface PanelSettings {
222
+ mode: 'light' | 'dark' | 'system'
223
+ showPicks: boolean
224
+ showLibrary: boolean
225
+ showCustom: boolean
226
+ showOverrides: boolean
227
+ showImportExport: boolean
228
+ draggable: boolean
229
+ animateDot: boolean
230
+ panelWidth: number | null
231
+ panelHeight: number | 'full' | null
232
+ libraryCollapsed: boolean
233
+ /** Whether themes re-colour the site's logo too. Off (the default) keeps its own colours. */
234
+ colourLogo: boolean
235
+ /** On a re-coloured site: 'colourful' paints its parts by role, 'subtle' only swaps its colours. */
236
+ colourStyle: 'colourful' | 'subtle'
237
+ /** Hidden on this device (from the finish screen); Alt+Shift+C or ?colorsbymax brings it back. */
238
+ hideButton: boolean
239
+ }
240
+ /** The visitor settings the panel starts with. */
241
+ export const DEFAULT_SETTINGS: PanelSettings
242
+
243
+ // ---------------------------------------------------------------- contrast
244
+
245
+ export type PairingKind = 'text' | 'large-text' | 'non-text'
246
+
247
+ export interface Pairing {
248
+ id: string
249
+ label: string
250
+ kind: PairingKind
251
+ fg(tokens: ThemeTokens): string
252
+ bg(tokens: ThemeTokens): string
253
+ /** Tokens auto-fix may adjust, in preference order. */
254
+ fixable: TokenKey[]
255
+ }
256
+
257
+ export interface ContrastIssue {
258
+ pairing: Pairing
259
+ ratio: number
260
+ required: number
261
+ /** e.g. "Primary text on background: 2.7:1 — text will be hard to read (needs 4.5:1)" */
262
+ message: string
263
+ }
264
+
265
+ /** Every foreground/background pairing colorsbymax checks. */
266
+ export const PAIRINGS: Pairing[]
267
+ export const MIN_CONTRAST_TEXT: number
268
+ export const MIN_CONTRAST_LARGE_TEXT: number
269
+ export const MIN_CONTRAST_NON_TEXT: number
270
+ export const MIN_RAMP_STEP_DELTA_E: number
271
+
272
+ /** Every failing pairing in a theme. */
273
+ export function checkTheme(tokens: ThemeTokens): ContrastIssue[]
274
+ /** The smallest lightness change to one token that makes a pairing pass, or null. */
275
+ export function suggestFix(pairing: Pairing, tokens: ThemeTokens): { key: TokenKey; value: string } | null
276
+ /** Fixes failing pairings until the theme passes or nothing more helps. Returns the changed tokens. */
277
+ export function fixAll(tokens: ThemeTokens): Partial<ThemeTokens>
278
+ /** Problems with an ordered colour ramp drawn on `surface`, as sentences. */
279
+ export function checkRamp(colors: string[], surface: string): string[]
280
+
281
+ // ---------------------------------------------------------------- colour helpers
282
+
283
+ /** WCAG 2 contrast ratio between two hex colours, 1–21. */
284
+ export function contrastRatio(a: string, b: string): number
285
+ /** "#abc", "abc" or "#AABBCC" → "#aabbcc"; null for anything else. */
286
+ export function normalizeHex(input: unknown): string | null
287
+
288
+ // ---------------------------------------------------------------- light and dark
289
+
290
+ /** True when a theme's page background is dark. */
291
+ export function isDarkTheme(tokens: ThemeTokens): boolean
292
+ /** Dark versions of a light theme's tokens, adjusted to pass contrast. */
293
+ export function darkTokens(tokens: ThemeTokens): ThemeTokens
294
+ /** The dark twin of a light theme (themes that are already dark come back unchanged). */
295
+ export function toDark<T extends Theme>(theme: T): T
296
+
297
+ // ---------------------------------------------------------------- site scan
298
+
299
+ export interface ColourTally {
300
+ hex: string
301
+ /** Area painted as a background. */
302
+ bg: number
303
+ /** Weighted amount of text in this colour. */
304
+ text: number
305
+ /** Border length in this colour. */
306
+ border: number
307
+ }
308
+
309
+ export type Roles = Partial<Record<TokenKey, string>>
310
+
311
+ /** The page's painted colours, ignoring any applied theme. `exclude` is a selector to skip. */
312
+ export function collectColors(options?: { exclude?: string }): ColourTally[]
313
+ /** Works out token roles from collected colours. */
314
+ export function inferRoles(colors: ColourTally[]): { roles: Roles; palette: string[] }
315
+ /** A complete theme from detected roles, deriving whatever wasn't found. */
316
+ export function themeFromRoles(
317
+ roles: Roles,
318
+ variant?: { softness?: number; boldness?: number; complementary?: boolean },
319
+ ): ThemeTokens
320
+ /** Themes suggested for a scanned site, named after it, plus the closest library themes. */
321
+ export function suggestThemes(scan: { roles: Roles }, siteName: string, library?: Theme[]): Theme[]
322
+ /** Best guess at the site's name: og:site_name, then the title, then the host. */
323
+ export function detectSiteName(doc?: Document): string
324
+
325
+ // ---------------------------------------------------------------- palettes from files
326
+
327
+ /** The main colours in an image (or a PDF, with `loadPdf`), most important first. */
328
+ export function coloursFromFile(
329
+ file: File,
330
+ options?: { loadPdf?: (() => Promise<unknown>) | null },
331
+ ): Promise<{ colours: string[]; from: 'image' | 'pdf-text' | 'pdf' }>
332
+ /** The dominant colours in sets of RGBA pixels, edge blends removed. */
333
+ export function dominantColours(pixelSets: Uint8ClampedArray[]): string[]
334
+ /** Assigns a palette's colours to theme roles. */
335
+ export function rolesFromPalette(colours: string[]): Roles
336
+ /** A complete, contrast-checked theme built around a palette. */
337
+ export function themeFromPalette(colours: string[]): ThemeTokens