hextimator 0.1.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/CHANGELOG.md +2 -0
- package/LICENSE.md +7 -0
- package/README.md +137 -0
- package/dist/HextimatePaletteBuilder-BzrgFryu.d.mts +540 -0
- package/dist/HextimatePaletteBuilder-BzrgFryu.d.ts +540 -0
- package/dist/chunk-MBVLFPTG.js +2142 -0
- package/dist/cli.js +2395 -0
- package/dist/index.d.mts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +20 -0
- package/dist/react.d.mts +132 -0
- package/dist/react.d.ts +132 -0
- package/dist/react.js +245 -0
- package/llms.txt +289 -0
- package/package.json +81 -0
- package/tailwind.css +22 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hextimate
|
|
3
|
+
} from "./chunk-MBVLFPTG.js";
|
|
4
|
+
|
|
5
|
+
// src/react.tsx
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
useSyncExternalStore
|
|
15
|
+
} from "react";
|
|
16
|
+
import { jsx } from "react/jsx-runtime";
|
|
17
|
+
function buildStyleContent(palette, darkMode, cssPrefix) {
|
|
18
|
+
const lightEntries = Object.entries(palette.light);
|
|
19
|
+
const darkEntries = Object.entries(palette.dark);
|
|
20
|
+
const toVars = (entries) => entries.map(([key, value]) => `${cssPrefix}${key}: ${value};`).join("\n ");
|
|
21
|
+
const lightVars = toVars(lightEntries);
|
|
22
|
+
const darkVars = toVars(darkEntries);
|
|
23
|
+
if (darkMode === false) {
|
|
24
|
+
return `:root {
|
|
25
|
+
${lightVars}
|
|
26
|
+
}`;
|
|
27
|
+
}
|
|
28
|
+
if (darkMode.type === "media") {
|
|
29
|
+
return [
|
|
30
|
+
`:root {
|
|
31
|
+
${lightVars}
|
|
32
|
+
}`,
|
|
33
|
+
`@media (prefers-color-scheme: dark) {
|
|
34
|
+
:root {
|
|
35
|
+
${darkVars}
|
|
36
|
+
}
|
|
37
|
+
}`
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
if (darkMode.type === "media-or-class") {
|
|
41
|
+
const cls = darkMode.className ?? "dark";
|
|
42
|
+
const lightCls = cls === "dark" ? "light" : `not-${cls}`;
|
|
43
|
+
return [
|
|
44
|
+
`:root {
|
|
45
|
+
${lightVars}
|
|
46
|
+
}`,
|
|
47
|
+
`@media (prefers-color-scheme: dark) {
|
|
48
|
+
:root:not(.${lightCls}) {
|
|
49
|
+
${darkVars}
|
|
50
|
+
}
|
|
51
|
+
}`,
|
|
52
|
+
`:root.${cls} {
|
|
53
|
+
${darkVars}
|
|
54
|
+
}`
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
if (darkMode.type === "class") {
|
|
58
|
+
const cls = darkMode.className ?? "dark";
|
|
59
|
+
return [`:root {
|
|
60
|
+
${lightVars}
|
|
61
|
+
}`, `.${cls} {
|
|
62
|
+
${darkVars}
|
|
63
|
+
}`].join(
|
|
64
|
+
"\n"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const attr = darkMode.attribute ?? "data-theme";
|
|
68
|
+
return [
|
|
69
|
+
`:root {
|
|
70
|
+
${lightVars}
|
|
71
|
+
}`,
|
|
72
|
+
`[${attr}="dark"] {
|
|
73
|
+
${darkVars}
|
|
74
|
+
}`
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
function buildTargetedVars(palette, darkMode, cssPrefix) {
|
|
78
|
+
const prefixEntries = (entries) => entries.map(
|
|
79
|
+
([key, value]) => [`${cssPrefix}${key}`, value]
|
|
80
|
+
);
|
|
81
|
+
return {
|
|
82
|
+
light: prefixEntries(
|
|
83
|
+
Object.entries(palette.light)
|
|
84
|
+
),
|
|
85
|
+
dark: darkMode !== false ? prefixEntries(Object.entries(palette.dark)) : []
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function useStableOptions(options) {
|
|
89
|
+
const ref = useRef(options);
|
|
90
|
+
const serialized = JSON.stringify({
|
|
91
|
+
generation: options?.generation,
|
|
92
|
+
format: options?.format,
|
|
93
|
+
darkMode: options?.darkMode,
|
|
94
|
+
cssPrefix: options?.cssPrefix
|
|
95
|
+
});
|
|
96
|
+
if (serialized !== JSON.stringify({
|
|
97
|
+
generation: ref.current?.generation,
|
|
98
|
+
format: ref.current?.format,
|
|
99
|
+
darkMode: ref.current?.darkMode,
|
|
100
|
+
cssPrefix: ref.current?.cssPrefix
|
|
101
|
+
})) {
|
|
102
|
+
ref.current = options;
|
|
103
|
+
}
|
|
104
|
+
return ref.current;
|
|
105
|
+
}
|
|
106
|
+
function useHextimator(color, options) {
|
|
107
|
+
const stable = useStableOptions(options);
|
|
108
|
+
const configure = options?.configure;
|
|
109
|
+
const target = options?.target;
|
|
110
|
+
const palette = useMemo(() => {
|
|
111
|
+
const builder = hextimate(color, stable?.generation);
|
|
112
|
+
configure?.(builder);
|
|
113
|
+
return builder.format({
|
|
114
|
+
...stable?.format,
|
|
115
|
+
as: "css"
|
|
116
|
+
});
|
|
117
|
+
}, [color, stable?.generation, stable?.format, configure]);
|
|
118
|
+
const darkMode = stable?.darkMode ?? { type: "media" };
|
|
119
|
+
const cssPrefix = stable?.cssPrefix ?? "";
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const el = target?.current;
|
|
122
|
+
if (el) {
|
|
123
|
+
const vars = buildTargetedVars(palette, darkMode, cssPrefix);
|
|
124
|
+
for (const [key, value] of vars.light) {
|
|
125
|
+
el.style.setProperty(key, value);
|
|
126
|
+
}
|
|
127
|
+
return () => {
|
|
128
|
+
for (const [key] of vars.light) {
|
|
129
|
+
el.style.removeProperty(key);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const style = document.createElement("style");
|
|
134
|
+
style.setAttribute("data-hextimator", "");
|
|
135
|
+
style.textContent = buildStyleContent(palette, darkMode, cssPrefix);
|
|
136
|
+
document.head.appendChild(style);
|
|
137
|
+
return () => {
|
|
138
|
+
document.head.removeChild(style);
|
|
139
|
+
};
|
|
140
|
+
}, [palette, darkMode, cssPrefix, target]);
|
|
141
|
+
return palette;
|
|
142
|
+
}
|
|
143
|
+
var darkQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
|
|
144
|
+
function useOsPrefersDark() {
|
|
145
|
+
return useSyncExternalStore(
|
|
146
|
+
(cb) => {
|
|
147
|
+
if (!darkQuery) return () => {
|
|
148
|
+
};
|
|
149
|
+
darkQuery.addEventListener("change", cb);
|
|
150
|
+
return () => darkQuery.removeEventListener("change", cb);
|
|
151
|
+
},
|
|
152
|
+
() => darkQuery?.matches ?? false,
|
|
153
|
+
() => false
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function applyModeToDOM(mode, darkMode) {
|
|
157
|
+
if (typeof document === "undefined") return;
|
|
158
|
+
if (darkMode === false || darkMode.type === "media") return;
|
|
159
|
+
const root = document.documentElement;
|
|
160
|
+
if (darkMode.type === "data") {
|
|
161
|
+
const attr = darkMode.attribute ?? "data-theme";
|
|
162
|
+
if (mode) {
|
|
163
|
+
root.setAttribute(attr, mode);
|
|
164
|
+
} else {
|
|
165
|
+
root.removeAttribute(attr);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const cls = darkMode.className ?? "dark";
|
|
170
|
+
const lightCls = cls === "dark" ? "light" : `not-${cls}`;
|
|
171
|
+
root.classList.remove(cls, lightCls);
|
|
172
|
+
if (mode) {
|
|
173
|
+
root.classList.add(mode === "dark" ? cls : lightCls);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
var HextimatorContext = createContext(null);
|
|
177
|
+
function HextimatorProvider({
|
|
178
|
+
children,
|
|
179
|
+
defaultColor,
|
|
180
|
+
defaultMode: initialMode = "system",
|
|
181
|
+
generation: initialGeneration,
|
|
182
|
+
format: formatOpts,
|
|
183
|
+
configure: initialConfigure,
|
|
184
|
+
darkMode,
|
|
185
|
+
cssPrefix,
|
|
186
|
+
target
|
|
187
|
+
}) {
|
|
188
|
+
const [color, setColor] = useState(defaultColor);
|
|
189
|
+
const [modePreference, setMode] = useState(initialMode);
|
|
190
|
+
const [generation, setGeneration] = useState(initialGeneration);
|
|
191
|
+
const [configure, setConfigureState] = useState(() => initialConfigure);
|
|
192
|
+
const osDark = useOsPrefersDark();
|
|
193
|
+
const mode = modePreference === "system" ? osDark ? "dark" : "light" : modePreference;
|
|
194
|
+
const resolvedDarkMode = darkMode ?? { type: "media" };
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
applyModeToDOM(
|
|
197
|
+
modePreference === "system" ? null : modePreference,
|
|
198
|
+
resolvedDarkMode
|
|
199
|
+
);
|
|
200
|
+
}, [modePreference, resolvedDarkMode]);
|
|
201
|
+
const setConfigure = useCallback(
|
|
202
|
+
(fn) => {
|
|
203
|
+
setConfigureState(() => fn);
|
|
204
|
+
},
|
|
205
|
+
[]
|
|
206
|
+
);
|
|
207
|
+
const palette = useHextimator(color, {
|
|
208
|
+
generation,
|
|
209
|
+
format: formatOpts,
|
|
210
|
+
configure,
|
|
211
|
+
darkMode,
|
|
212
|
+
cssPrefix,
|
|
213
|
+
target
|
|
214
|
+
});
|
|
215
|
+
const value = useMemo(
|
|
216
|
+
() => ({
|
|
217
|
+
color,
|
|
218
|
+
setColor,
|
|
219
|
+
mode,
|
|
220
|
+
modePreference,
|
|
221
|
+
setMode,
|
|
222
|
+
generation,
|
|
223
|
+
setGeneration,
|
|
224
|
+
configure,
|
|
225
|
+
setConfigure,
|
|
226
|
+
palette
|
|
227
|
+
}),
|
|
228
|
+
[color, mode, modePreference, generation, configure, setConfigure, palette]
|
|
229
|
+
);
|
|
230
|
+
return /* @__PURE__ */ jsx(HextimatorContext.Provider, { value, children });
|
|
231
|
+
}
|
|
232
|
+
function useHextimatorTheme() {
|
|
233
|
+
const ctx = useContext(HextimatorContext);
|
|
234
|
+
if (!ctx) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
"useHextimatorTheme must be used within a <HextimatorProvider>"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return ctx;
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
HextimatorProvider,
|
|
243
|
+
useHextimator,
|
|
244
|
+
useHextimatorTheme
|
|
245
|
+
};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# hextimator
|
|
2
|
+
|
|
3
|
+
> Generate perceptually uniform color palettes with light/dark themes and WCAG contrast guarantees from a single brand color. Outputs CSS variables, Tailwind tokens, SCSS variables, JSON, or plain objects. Designed for runtime use in multi-tenant apps.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hextimator
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { hextimate } from "hextimator";
|
|
15
|
+
|
|
16
|
+
// Minimal: one color in, full theme out
|
|
17
|
+
const theme = hextimate("#6A5ACD").format();
|
|
18
|
+
// → { light: { accent: "#6a5acd", "accent-strong": "...", ... }, dark: { ... } }
|
|
19
|
+
|
|
20
|
+
// With format options
|
|
21
|
+
const css = hextimate("#6A5ACD").format({ as: "css", colors: "oklch" });
|
|
22
|
+
// → { light: { "--accent": "oklch(...)", ... }, dark: { ... } }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
### `hextimate(color, options?)`
|
|
28
|
+
|
|
29
|
+
Creates a palette builder from any color input.
|
|
30
|
+
|
|
31
|
+
- `color`: `string` (hex, CSS function), `[r, g, b]` tuple, `number` (0xFF6666), or a `Color` object
|
|
32
|
+
- `options`: `HextimateGenerationOptions` (see below)
|
|
33
|
+
- Returns: `HextimatePaletteBuilder` (chainable)
|
|
34
|
+
- Throws: `HextimateError` on invalid input
|
|
35
|
+
|
|
36
|
+
### Builder methods (all return `this`, chainable)
|
|
37
|
+
|
|
38
|
+
| Method | Description |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `.addRole(name, color)` | Add a new color role with DEFAULT, strong, weak, foreground variants |
|
|
41
|
+
| `.addVariant(name, placement)` | Add a lightness step to every role. `placement`: `{ from: "strong" }`, `{ from: "weak" }`, or `{ between: ["DEFAULT", "weak"] }` |
|
|
42
|
+
| `.addToken(name, value)` | Add a standalone derived token. `value`: a color, `{ from: "role.variant", emphasis?, lightness?, chroma?, hue? }`, or `{ light: ..., dark: ... }` for per-theme values. `emphasis` is theme-aware (positive = more contrast, negative = softer) |
|
|
43
|
+
| `.preset(preset)` | Apply a framework preset (e.g. `presets.shadcn`). Preset format defaults are overridden by `.format()` options |
|
|
44
|
+
| `.simulate(cvdType, severity?)` | Simulate color vision deficiency. Types: `protanopia`, `deuteranopia`, `tritanopia`, `achromatopsia` |
|
|
45
|
+
| `.adaptFor(cvdType, severity?)` | Optimize palette for a CVD type (use instead of simulate for actual accessibility) |
|
|
46
|
+
| `.fork(color?, options?)` | Clone builder with optional new color/options. Original unchanged |
|
|
47
|
+
| `.format(options?)` | Output the palette (see format options) |
|
|
48
|
+
|
|
49
|
+
### `.format(options?)` → `{ light: FormatResult, dark: FormatResult }`
|
|
50
|
+
|
|
51
|
+
| Option | Values | Default |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `as` | `"object"`, `"css"`, `"tailwind"`, `"tailwind-css"`, `"scss"`, `"json"` | `"object"` |
|
|
54
|
+
| `colors` | `"hex"`, `"rgb"`, `"rgb-raw"`, `"hsl"`, `"hsl-raw"`, `"oklch"`, `"oklch-raw"`, `"p3"`, `"p3-raw"` | `"hex"` |
|
|
55
|
+
| `roleNames` | `Record<string, string>` — rename roles in output keys | built-in names |
|
|
56
|
+
| `variantNames` | `Record<string, string>` — rename variant suffixes | built-in names |
|
|
57
|
+
| `separator` | `string` — between role and variant in keys | `"-"` |
|
|
58
|
+
|
|
59
|
+
Output shape depends on `as`:
|
|
60
|
+
- `"object"` → `Record<string, string>` flat map: `{ accent: "#...", "accent-strong": "#..." }`
|
|
61
|
+
- `"css"` → `Record<string, string>` flat map: `{ "--accent": "#...", "--accent-strong": "#..." }`
|
|
62
|
+
- `"tailwind"` → nested map: `{ accent: { DEFAULT: "#...", strong: "#..." } }`
|
|
63
|
+
- `"tailwind-css"` → string: `@theme { --color-accent: #...; }`
|
|
64
|
+
- `"scss"` → `Record<string, string>` flat map: `{ "$accent": "#...", "$accent-strong": "#..." }`
|
|
65
|
+
- `"json"` → JSON string
|
|
66
|
+
|
|
67
|
+
### Generation options
|
|
68
|
+
|
|
69
|
+
| Option | Type | Default | Description |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `baseColor` | color input | auto-derived | Override base/neutral baseline color |
|
|
72
|
+
| `baseHueShift` | degrees | `0` | Rotate base hue relative to accent. Ignored if `baseColor` set |
|
|
73
|
+
| `invertDarkModeBaseAccent` | boolean | `false` | Swap base/accent hues in dark mode. Useful when the accent should become the background in dark mode. Requires `baseColor` |
|
|
74
|
+
| `semanticColors` | `{ positive?, negative?, warning? }` | auto | Override semantic colors (green/red/amber) |
|
|
75
|
+
| `semanticColorRanges` | `{ positive?: [start,end], ... }` | greens, reds, ambers | Hue ranges for semantic detection |
|
|
76
|
+
| `baseMaxChroma` | number | `0.01` | Max chroma for base colors |
|
|
77
|
+
| `foregroundMaxChroma` | number | `0.01` | Max chroma for foreground colors |
|
|
78
|
+
| `light` | `{ lightness?, maxChroma? }` | `{ lightness: 0.7 }` | Light theme adjustments |
|
|
79
|
+
| `dark` | `{ lightness?, maxChroma? }` | `{ lightness: 0.6 }` | Dark theme adjustments |
|
|
80
|
+
| `minContrastRatio` | `"AAA"`, `"AA"`, or number | `"AAA"` | WCAG contrast target (AAA=7, AA=4.5) |
|
|
81
|
+
| `hueShift` | degrees | `0` | Per-variant hue rotation |
|
|
82
|
+
|
|
83
|
+
### Default palette structure
|
|
84
|
+
|
|
85
|
+
Every palette includes 5 roles × 4 variants = 20 tokens:
|
|
86
|
+
|
|
87
|
+
- Roles: `base`, `accent`, `positive`, `negative`, `warning`
|
|
88
|
+
- Variants: `DEFAULT` (no suffix), `strong`, `weak`, `foreground`
|
|
89
|
+
|
|
90
|
+
Token keys: `accent`, `accent-strong`, `accent-weak`, `accent-foreground`, `base`, `base-strong`, ...
|
|
91
|
+
|
|
92
|
+
### Presets
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { hextimate, presets } from "hextimator";
|
|
96
|
+
|
|
97
|
+
const theme = hextimate("#6366F1")
|
|
98
|
+
.preset(presets.shadcn)
|
|
99
|
+
.format();
|
|
100
|
+
|
|
101
|
+
// Override preset defaults:
|
|
102
|
+
const theme = hextimate("#6366F1")
|
|
103
|
+
.preset(presets.shadcn)
|
|
104
|
+
.format({ colors: "hsl-raw" });
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The shadcn preset adds tokens like foreground, card, popover, secondary, muted, border, input, ring, and renames roles (base→background, accent→primary, negative→destructive, positive→success).
|
|
108
|
+
|
|
109
|
+
### Standalone tokens with per-theme values
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
hextimate("#6366F1")
|
|
113
|
+
.addToken("border", {
|
|
114
|
+
light: { from: "base.weak", lightness: -0.08 },
|
|
115
|
+
dark: { from: "base.weak", lightness: +0.08 },
|
|
116
|
+
})
|
|
117
|
+
.addToken("ring", { from: "accent" })
|
|
118
|
+
.format({ as: "css" });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Utility exports
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { parseColor, convertColor } from "hextimator";
|
|
125
|
+
|
|
126
|
+
const color = parseColor("rgb(255, 102, 102)"); // → Color object (sRGB)
|
|
127
|
+
const oklch = convertColor(color, "oklch"); // → OKLCH object
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Also exported: `daltonizeColor(color, cvdType, severity?)`, `simulateColor(color, cvdType, severity?)`.
|
|
131
|
+
|
|
132
|
+
### Color input formats
|
|
133
|
+
|
|
134
|
+
All of these are valid:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
hextimate("#FF6666"); // hex string
|
|
138
|
+
hextimate("FF6666"); // hex without #
|
|
139
|
+
hextimate("rgb(255, 102, 102)"); // CSS function
|
|
140
|
+
hextimate("hsl(0, 100%, 70%)"); // CSS HSL
|
|
141
|
+
hextimate([255, 102, 102]); // RGB tuple
|
|
142
|
+
hextimate(0xff6666); // numeric hex
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Alpha is always ignored (forced to 1) to preserve contrast guarantees.
|
|
146
|
+
|
|
147
|
+
## CLI
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx hextimator <color> [options]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
| Flag | Description | Default |
|
|
154
|
+
|---|---|---|
|
|
155
|
+
| `-f, --format` | `css`, `object`, `tailwind`, `tailwind-css`, `scss`, `json` | `css` |
|
|
156
|
+
| `-c, --colors` | `hex`, `rgb`, `rgb-raw`, `hsl`, `hsl-raw`, `oklch`, `oklch-raw`, `p3`, `p3-raw` | `hex` |
|
|
157
|
+
| `-t, --theme` | `light`, `dark`, `both` | `both` |
|
|
158
|
+
| `-p, --preset` | Apply preset (e.g. `shadcn`) | — |
|
|
159
|
+
| `--separator` | Token separator between role and variant | `-` |
|
|
160
|
+
| `--base-color` | Override base color | — |
|
|
161
|
+
| `--base-hue-shift` | Degrees | `0` |
|
|
162
|
+
| `--hue-shift` | Per-variant hue shift | `0` |
|
|
163
|
+
| `--base-max-chroma` | Max chroma for base | `0.01` |
|
|
164
|
+
| `--fg-max-chroma` | Max chroma for foreground | `0.01` |
|
|
165
|
+
| `--light-lightness` | Light theme lightness 0-1 | `0.7` |
|
|
166
|
+
| `--light-max-chroma` | Light theme max chroma | — |
|
|
167
|
+
| `--dark-lightness` | Dark theme lightness 0-1 | `0.6` |
|
|
168
|
+
| `--dark-max-chroma` | Dark theme max chroma | — |
|
|
169
|
+
| `--min-contrast` | `AAA`, `AA`, or number | `AAA` |
|
|
170
|
+
| `--invert-dark` | Swap base/accent hues in dark mode (requires --base-color) | — |
|
|
171
|
+
| `--positive` | Override positive/success color | auto |
|
|
172
|
+
| `--negative` | Override negative/error color | auto |
|
|
173
|
+
| `--warning` | Override warning color | auto |
|
|
174
|
+
| `--simulate` | Simulate CVD: `protanopia`, `deuteranopia`, `tritanopia`, `achromatopsia` | — |
|
|
175
|
+
| `--adapt` | Adapt palette for CVD type | — |
|
|
176
|
+
| `--cvd-severity` | CVD severity 0-1 | `1` |
|
|
177
|
+
| `--role <name>=<color>` | Add custom role (repeatable) | — |
|
|
178
|
+
| `--variant <spec>` | Add variant: `name:from:edge` or `name:between:a,b` (repeatable) | — |
|
|
179
|
+
| `-o, --output <path>` | Write to file instead of stdout | stdout |
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Generate CSS palette
|
|
183
|
+
npx hextimator "#6A5ACD" --format css --colors oklch
|
|
184
|
+
|
|
185
|
+
# shadcn/ui preset
|
|
186
|
+
npx hextimator "#6366F1" --preset shadcn
|
|
187
|
+
|
|
188
|
+
# Invert base/accent in dark mode
|
|
189
|
+
npx hextimator "#6A5ACD" --base-color "#FEBA5D" --invert-dark
|
|
190
|
+
|
|
191
|
+
# Custom roles and variants
|
|
192
|
+
npx hextimator "#22aa44" --role cta=#ee2244 --variant hover:from:strong -o theme.css
|
|
193
|
+
|
|
194
|
+
# Simulate color blindness
|
|
195
|
+
npx hextimator "#ff6600" --simulate deuteranopia
|
|
196
|
+
|
|
197
|
+
# Adapt for color blindness
|
|
198
|
+
npx hextimator "#ff6600" --adapt deuteranopia --cvd-severity 0.8
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## React
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { useHextimator } from "hextimator/react";
|
|
205
|
+
|
|
206
|
+
function App() {
|
|
207
|
+
useHextimator("#6A5ACD");
|
|
208
|
+
return <div className="bg-accent text-accent-foreground">Themed!</div>;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Dark mode strategies: `{ type: "media" }` (default), `{ type: "class" }`, `{ type: "data" }`, or `false`.
|
|
213
|
+
|
|
214
|
+
Options: `generation`, `format`, `configure` (access builder), `darkMode`, `cssPrefix`, `target` (ref for scoped theming).
|
|
215
|
+
|
|
216
|
+
Provider for app-wide theming:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { HextimatorProvider, useHextimatorTheme } from "hextimator/react";
|
|
220
|
+
|
|
221
|
+
// Wrap app
|
|
222
|
+
<HextimatorProvider defaultColor="#6A5ACD">
|
|
223
|
+
<App />
|
|
224
|
+
</HextimatorProvider>
|
|
225
|
+
|
|
226
|
+
// In children — setColor triggers re-generation
|
|
227
|
+
const { color, setColor, palette } = useHextimatorTheme();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Tailwind CSS v4
|
|
231
|
+
|
|
232
|
+
```css
|
|
233
|
+
@import "tailwindcss";
|
|
234
|
+
@import "hextimator/tailwind.css";
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This registers all 20 built-in tokens as Tailwind utilities (`bg-accent`, `text-base-foreground`, etc.).
|
|
238
|
+
|
|
239
|
+
## Common recipes
|
|
240
|
+
|
|
241
|
+
### Inverted dark mode (accent as background)
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
const theme = hextimate("#52FE8C", {
|
|
245
|
+
baseColor: "#FEBA5D",
|
|
246
|
+
invertDarkModeBaseAccent: true,
|
|
247
|
+
}).format({ as: "css" });
|
|
248
|
+
// Dark mode uses the accent hue as background, base hue as accent
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### shadcn/ui theme
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { hextimate, presets } from "hextimator";
|
|
255
|
+
|
|
256
|
+
hextimate("#6366F1")
|
|
257
|
+
.preset(presets.shadcn)
|
|
258
|
+
.format();
|
|
259
|
+
// Generates all shadcn/ui tokens with proper naming
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Multi-tenant with `.fork()`
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const base = hextimate("#52FE8C", { baseColor: "#FEBA5D" })
|
|
266
|
+
.addRole("banner", "#ff006e")
|
|
267
|
+
.addVariant("hover", { from: "strong" });
|
|
268
|
+
|
|
269
|
+
const tenantA = base.fork("#ff6677").format({ as: "css" });
|
|
270
|
+
const tenantB = base.fork("#3a86ff").format({ as: "css" });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Color vision deficiency
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
const normalTheme = hextimate("#ff6600");
|
|
277
|
+
const adaptedTheme = normalTheme.fork().adaptFor("deuteranopia");
|
|
278
|
+
const preview = normalTheme.fork().simulate("deuteranopia");
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Optional
|
|
282
|
+
|
|
283
|
+
- [Extending the palette](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/extending-the-palette.md): addRole, addVariant, addToken in depth
|
|
284
|
+
- [Customization reference](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/customization.md): all generation and format options
|
|
285
|
+
- [Multiple themes](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/multiple-themes.md): .fork() and dynamic theming
|
|
286
|
+
- [Color vision deficiency](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/color-vision-deficiency.md): simulate and adaptFor
|
|
287
|
+
- [React docs](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/react.md): hook, provider, dark mode strategies
|
|
288
|
+
- [Tailwind CSS v4](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/tailwind.md): setup and custom tokens
|
|
289
|
+
- [Real-world examples](https://github.com/fgrgic/hextimator/blob/main/packages/hextimator/docs/examples.md): shadcn/ui, Stripe, Slack configs
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hextimator",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate perceptually uniform color palettes with light/dark themes and WCAG contrast guarantees from a single brand color. Outputs CSS vars, Tailwind, SCSS, or JSON.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"color",
|
|
7
|
+
"palette",
|
|
8
|
+
"generator",
|
|
9
|
+
"creator",
|
|
10
|
+
"oklch",
|
|
11
|
+
"rgb",
|
|
12
|
+
"rgb2oklch",
|
|
13
|
+
"theme",
|
|
14
|
+
"design-tokens",
|
|
15
|
+
"dark-mode",
|
|
16
|
+
"wcag",
|
|
17
|
+
"accessibility",
|
|
18
|
+
"contrast",
|
|
19
|
+
"brand-color",
|
|
20
|
+
"css-variables",
|
|
21
|
+
"tailwind-theme",
|
|
22
|
+
"runtime-theming",
|
|
23
|
+
"multi-tenant"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://hextimator.com",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/fgrgic/hextimator/issues"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/fgrgic/hextimator.git"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.3.0"
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"hextimator": "./dist/cli.js",
|
|
39
|
+
"hextimate": "./dist/cli.js"
|
|
40
|
+
},
|
|
41
|
+
"author": "fgrgic",
|
|
42
|
+
"type": "module",
|
|
43
|
+
"module": "dist/index.js",
|
|
44
|
+
"types": "dist/index.d.ts",
|
|
45
|
+
"exports": {
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./dist/index.d.ts",
|
|
48
|
+
"import": "./dist/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./react": {
|
|
51
|
+
"types": "./dist/react.d.ts",
|
|
52
|
+
"import": "./dist/react.js"
|
|
53
|
+
},
|
|
54
|
+
"./tailwind.css": "./tailwind.css"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"dist",
|
|
58
|
+
"tailwind.css",
|
|
59
|
+
"llms.txt",
|
|
60
|
+
"CHANGELOG.md"
|
|
61
|
+
],
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"dev": "tsup --watch",
|
|
65
|
+
"test": "bun test"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"react": ">=18"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"react": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/react": "^19.2.7",
|
|
77
|
+
"react": "^19.2.0",
|
|
78
|
+
"tsup": "^8.0.0",
|
|
79
|
+
"typescript": "^5.0.0"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/tailwind.css
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@theme inline {
|
|
2
|
+
--color-accent: var(--accent);
|
|
3
|
+
--color-accent-strong: var(--accent-strong);
|
|
4
|
+
--color-accent-weak: var(--accent-weak);
|
|
5
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
6
|
+
--color-base: var(--base);
|
|
7
|
+
--color-base-strong: var(--base-strong);
|
|
8
|
+
--color-base-weak: var(--base-weak);
|
|
9
|
+
--color-base-foreground: var(--base-foreground);
|
|
10
|
+
--color-positive: var(--positive);
|
|
11
|
+
--color-positive-strong: var(--positive-strong);
|
|
12
|
+
--color-positive-weak: var(--positive-weak);
|
|
13
|
+
--color-positive-foreground: var(--positive-foreground);
|
|
14
|
+
--color-negative: var(--negative);
|
|
15
|
+
--color-negative-strong: var(--negative-strong);
|
|
16
|
+
--color-negative-weak: var(--negative-weak);
|
|
17
|
+
--color-negative-foreground: var(--negative-foreground);
|
|
18
|
+
--color-warning: var(--warning);
|
|
19
|
+
--color-warning-strong: var(--warning-strong);
|
|
20
|
+
--color-warning-weak: var(--warning-weak);
|
|
21
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
22
|
+
}
|