# css-magic-gradient — AI Reference TypeScript library for generating CSS gradient strings — linear, radial, conic, presets, color-harmony generators, WCAG accessibility utilities, CSS-variable extraction/resolution, and canvas/image export. Single runtime dependency (`color-value-tools`); Vue 3 and React are optional peers, each with its own subpath entry, and neither is ever pulled in by the core entry point. Version 1.3.0+ (this document reflects source with 6 real bugs fixed via PR #22 — if you're reading an installed `dist/` older than 1.3.0, expect the specific behaviors called out as "fixed"/"as of this fix" below to instead match their "before" state, described alongside each one). This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs). For human-readable narrative docs, see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/css-magic-gradient/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/css-magic-gradient/guide/overview - GitHub: https://github.com/macrulezru/css-magic-gradient - npm: https://www.npmjs.com/package/css-magic-gradient Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map | Import path | Contains | |---|---| | `css-magic-gradient` | Linear/radial/conic generators, 15 fixed presets, 10 dynamic harmony/palette generators, WCAG accessibility utilities, CSS-variable helpers, canvas/image export. Zero Vue/React code — confirmed no `@vue/*`/`react` import anywhere in this entry's module graph. | | `css-magic-gradient/vue` | 15 reactive `use*` hooks (`ComputedRef`) + a Vue plugin (`app.config.globalProperties.$use*`). Real `import ... from '@vue/runtime-core'` — never import this subpath in a project without `vue` installed. | | `css-magic-gradient/react` | The same 15 hooks, plain `string` via `useMemo`. No Vue dependency. | All three entries compile from `tsc` (plain `NodeNext` CJS/ESM dual-ish output per Node's own resolution, no bundler) — `dist/.js` mirrors `src/.ts` 1:1, no bundling/minification step. `baseColor` across nearly every generator accepts hex, `rgb()`, `hsl()`, named CSS colors, and CSS variables (`var(--x)`/`var(--x, fallback)`) — all funneled through this package's own `resolveBaseColor()` (section 9), not used raw. --- ## 2. Linear gradients (`linear-gradient.ts`) ```ts type GradientDirection = 'to bottom' | 'to top' | 'to right' | 'to left' | string type ColorInterpolation = 'srgb' | 'oklch' | 'lab' | 'hsl' | 'oklab' | 'lch' type ScaleInterpolation = 'rgb' | 'hsl' | 'oklab' | 'oklch' // subset createColorScale() accepts — no lab/lch interface GradientOptions { offsetPercent?: number // default 15 — brightness offset for the auto-generated light stop direction?: GradientDirection // default 'to bottom' angle?: number // takes precedence over direction when provided (see gotcha) fallbackColor?: string // default '#f5e477' — used when baseColor can't be resolved interpolation?: ColorInterpolation repeating?: boolean // repeating-linear-gradient instead of linear-gradient } interface CustomLinearGradientOptions { direction?: GradientDirection; angle?: number interpolation?: ColorInterpolation; repeating?: boolean } function createLinearGradient(baseColor: string, options?: GradientOptions): string function createLinearGradient(stops: ColorStop[], options?: CustomLinearGradientOptions): string function createMultiStepLinearGradient(baseColor: string, steps=3, options?: Omit & {direction?:string}): string function createMixedLinearGradient(colorA: string, colorB: string, steps=5, options?: Omit): string ``` - Overload 1 (`baseColor: string`) auto-generates a 2-stop gradient: `startColor = adjustHexBrightness(resolved.hex, offsetPercent)` (the lighter stop) → `endColor` (the base color itself, or the original `var(...)` expression when `isCssVar`, preserving the CSS variable in the actual output string). - Overload 2 (`stops: ColorStop[]`) uses the stops verbatim via `colorStopToString()` (section 9) — no brightness auto-generation, `offsetPercent`/`fallbackColor` don't apply to this overload. - **`angle`, when provided, takes precedence over `direction`** — as of this fix, checked via `angle !== undefined`. **Before the fix**, the check was the bare-truthy `angle ? ... : direction`, so `angle: 0` (a perfectly valid `0deg`) was silently treated as "not provided" and fell back to `direction` instead — present in all four functions in this file that accept `angle` (both `createLinearGradient` overloads, `createMultiStepLinearGradient`, `createMixedLinearGradient`). - `createMultiStepLinearGradient`: `percent = offsetPercent * (1 - i/(steps-1))` per stop, `i` from `0` to `steps-1` — no `steps===1` guard here (unlike `createMixedLinearGradient`, which explicitly special-cases `steps===1` → `t=0`). Passing `steps: 1` produces `NaN` in the percent calculation, which `adjustHexBrightness()` from `color-value-tools` silently absorbs back into the unmodified input hex (verified: `adjustHexBrightness('#3498db', NaN) === '#3498db'`) — so the practical effect is a degenerate 1-stop gradient using the raw base color, not a visible crash or literal `"NaN"` in the output. - `createMixedLinearGradient` uses `mixColors(a, b, t, {mode:'hsl', format:'hex'})` per step — HSL interpolation, not the CSS Color 4 `interpolation` option (that only affects the `in ` suffix on the generated `linear-gradient()`/`repeating-linear-gradient()` string itself, applied at render time by the browser — two separate interpolation concepts, don't conflate them). --- ## 3. Radial gradients (`radial-gradient.ts`) ```ts type RadialShape = 'circle' | 'ellipse' type RadialSize = 'closest-side'|'closest-corner'|'farthest-side'|'farthest-corner'|string|{width:string;height:string} type RadialHarmonyType = 'complementary' | 'triadic' | 'tetradic' | 'analogous' // NOTE: no 'split-complementary' — asymmetric vs. presets.ts's 5 harmony generators interface RadialGradientOptions { offsetPercent?: number // default 15 fallbackColor?: string // default '#f5e477' shape?: RadialShape // default 'ellipse' size?: RadialSize // default 'farthest-corner' position?: string // default 'center' colors?: ColorStop[] layers?: RadialGradientLayer[] repeating?: boolean harmonyType?: RadialHarmonyType interpolationSpace?: ScaleInterpolation // default 'oklch' } interface RadialGradientLayer { shape?; size?; position?; colors: ColorStop[] } function createRadialGradient(baseColor: string, options?: RadialGradientOptions): string function createRadialGradientLayers(baseColor: string, options?: { count?: number; minSizePercent?: number; maxSizePercent?: number // defaults 3, 20, 100 harmonyType?: RadialHarmonyType; interpolationSpace?: ScaleInterpolation; position?: string }): RadialGradientLayer[] ``` Mode priority inside `createRadialGradient` (first match wins): `layers` → `harmonyType` → `colors` (explicit stops) → auto-generate 2-stop from `baseColor`. All four correctly resolve `baseColor` through `resolveBaseColor()` before use — this file was never affected by the harmony-generator bug described in section 5/10. `createRadialGradientLayers()` builds a shrinking set of nested-ring layers (`sizeStep = (max-min)/(count-1)`, guarded to `0` when `count<=1`), feed the result straight into `createRadialGradient({ layers })`. --- ## 4. Conic gradients (`conic-gradient.ts`) ```ts type ConicHarmonyType = 'complementary' | 'triadic' | 'tetradic' | 'analogous' interface ConicGradientOptions { fromAngle?: number // default 0 position?: string // default '50% 50%' fallbackColor?: string // default '#f5e477' colors?: ColorStop[] colorScale?: string[] // interpolate through an arbitrary color list around the full circle hueRotation?: boolean // rotate hue evenly instead of adjusting brightness harmonyType?: ConicHarmonyType interpolationSpace?: ScaleInterpolation // default 'oklch' steps?: number // default 8 offsetPercent?: number // default 20 repeating?: boolean } function createConicGradient(baseColor: string, options?: ConicGradientOptions): string function createRainbowConicGradient(options?: { fromAngle?: number; position?: string; saturation?: number; lightness?: number // defaults 0, '50% 50%', 80, 60 steps?: number; repeating?: boolean // default 12 }): string ``` Mode priority (first match wins): `colorScale` (≥2 colors) → `harmonyType` → `colors` (explicit) → `hueRotation` → brightness-ramp (default). `colorScale`/`harmonyType` both close the loop by appending the first color again at the end before calling `createColorScale()`, so the gradient wraps seamlessly at 360°. **Default brightness-ramp mode, `steps: 1`**: as of this fix, `percent = steps === 1 ? offsetPercent : offsetPercent * (1 - i/(steps-1))` — a single stop at the full `offsetPercent` brightness, consistent with what the `i=0` term already computes for `steps>1`. **Before the fix**, the `steps===1` case always divided by `steps-1` unconditionally (`0/0 = NaN`), and `adjustHexBrightness(hex, NaN)` silently returned the unmodified base color — same absorption behavior as the linear-gradient `steps` bug in section 2, produced a 1-stop gradient with the wrong (unlightened) color rather than a crash or visible `NaN`. `hueRotation` mode has no such bug (`degrees = i*360/steps`, division by `steps` itself, never `steps-1`). --- ## 5. Fixed presets (`presets.ts`) — 15 ready-to-use gradient strings ```ts sunsetGradient, oceanGradient, auroraGradient, fireGradient, midnightGradient, peachGradient, mintGradient, rainbowGradient, glowGradient, forestGradient, goldenHourGradient, neonGradient, nordicGradient, pastelGradient, deepSpaceGradient ``` All are plain exported `string` constants, computed once at module load via `createLinearGradient`/`createRainbowConicGradient`/ `createRadialGradient` with hardcoded hex stops — no runtime cost beyond import, no options, not functions. --- ## 6. Dynamic harmony & palette generators (`presets.ts`) ```ts interface HarmonyGradientOptions { direction?: string; angle?: number; steps?: number; interpolationSpace?: ScaleInterpolation /* default 'oklch' */ } createComplementaryGradient(baseColor, options?: HarmonyGradientOptions): string // direction default 'to right', steps default 5 — interpolateColors(base, complement(base), steps, {space, format:'hex'}) createTriadicGradient(baseColor, options?: HarmonyGradientOptions & {smoothness?: number}): string // smoothness default 3 (= just the 3 anchor colors, no in-between interpolation) // totalSteps = Math.max(3, smoothness); steps===3 skips createColorScale entirely (3 raw anchors) createAnalogousGradient(baseColor, options?: HarmonyGradientOptions & {spread?: number}): string // spread passed straight to color-value-tools' analogous(base, spread) — default spread is THAT function's own default (30), not redeclared here // options.steps default 3 (read via options?.steps ?? 3, not destructured with the rest — same effective default) createTetradicGradient(baseColor, options?: HarmonyGradientOptions & {type?: 'linear'|'radial'|'conic'}): string // type default 'linear', steps default 9 // type:'conic' builds a raw `conic-gradient(from 0deg at 50% 50%, ...)` string directly — bypasses createConicGradient() entirely, so fromAngle/position/repeating are NOT configurable in this mode // type:'radial' delegates to createRadialGradient(resolvedHex, { colors: scale }) createSplitComplementaryGradient(baseColor, options?: HarmonyGradientOptions): string // steps default 5; steps===3 skips createColorScale (3 raw anchors), same pattern as triadic createMonochromaticGradient(baseColor, steps=5, options?: {direction?; angle?}): string // direction default 'to bottom' (NOT 'to right' like the other harmony generators) // colors from color-value-tools' colorShades(hex, steps) — HSL lightness 100→0, see that package's own ai-reference.txt section 9 createHueWheelGradient(baseColor, options?: {fromAngle?; position?; steps?}): string // thin wrapper: createConicGradient(baseColor, { hueRotation: true, ...options }) createTintGradient(baseColor, steps=5, options?: {direction?; angle?}): string // direction default 'to right'; tints(hex, steps) — Oklab, → white createShadeGradient(baseColor, steps=5, options?: {direction?; angle?}): string // direction default 'to right'; shades(hex, steps) — Oklab, → black createToneGradient(baseColor, steps=5, options?: {direction?; angle?; gray?}): string // direction default 'to right'; tones(hex, steps, gray) — Oklab, → gray ``` **All six of `createTriadicGradient` / `createAnalogousGradient` / `createTetradicGradient` / `createSplitComplementaryGradient` / `createMonochromaticGradient` now resolve `baseColor` through this package's own `resolveBaseColor(baseColor, '#f5e477')` before handing the hex off to `color-value-tools`' harmony/shade functions — as of this fix.** `createComplementaryGradient` and the tint/shade/tone/hue-wheel generators were never affected (they already called `resolveBaseColor` or delegated to a function that does). **Before the fix**, those five passed `baseColor` straight through to `color-value-tools`' `triadic()`/`analogous()`/`tetradic()`/ `splitComplementary()`/`colorShades()`, which each call `color-value-tools`' own internal `normalizeColor()` with no fallback parameter at all — an unresolvable input (most notably a CSS variable, `var(--brand, #3498db)`, which `normalizeColor()` doesn't parse) fell through to `color-value-tools`' hardcoded `'#000000'` fallback instead of this package's own `'#f5e477'` convention that every other generator in the library honors. Concretely: `createTriadicGradient('var(--x)')` used to return `linear-gradient(to right, #000000, #000000, #000000)`. **Gap — no reactive hook coverage for 2 of these 10 generators**: neither `css-magic-gradient/vue` nor `css-magic-gradient/react` exports a `useMonochromaticGradient` or `useHueWheelGradient` — both hook files have exactly 15 `use*` exports covering every *other* generator in this file (and the linear/radial/conic/accessibility generators), but these two are call-`createXGradient-in-a-computed-yourself` only. Confirmed by grepping both `vue-gradient-plugin.ts` and `react-gradient-plugin.ts` for every `export function use` — identical 15-function list in both, neither name present. --- ## 7. Accessibility / WCAG (`accessibility.ts`) ```ts type WcagLevel = 'AAA' | 'AA' | 'AA-large' | 'fail' interface AccessibleGradientOptions { direction?: GradientDirection; angle?: number targetLevel?: WcagLevel // default 'AA' interpolation?: ColorInterpolation; repeating?: boolean adjustmentStrategy?: 'lightness' | 'saturation' | 'both' // default 'lightness' } interface GradientWcagReport { level: WcagLevel; minContrast: number; problematicStops: number[] /* 0-1 positions failing 4.5 threshold, rounded to 2dp */ } interface BestTextColorDetail { recommended: '#000000'|'#ffffff'; black: {contrast:number; wcag:WcagLevel}; white: {contrast:number; wcag:WcagLevel} } function bestGradientTextColor(colorA: string, colorB: string): '#000000'|'#ffffff' function bestGradientTextColor(colors: string[], options?: {detailed?: false}): '#000000'|'#ffffff' function bestGradientTextColor(colors: string[], options: {detailed: true}): BestTextColorDetail function gradientContrastRatio(textColor: string, colorA: string, colorB: string): number function gradientContrastRatio(textColor: string, colors: string[]): number function gradientWcagLevel(textColor: string, colorA: string, colorB: string): GradientWcagReport function gradientWcagLevel(textColor: string, colors: string[]): GradientWcagReport function createAccessibleGradient(baseColor: string, textColor: string, options?: AccessibleGradientOptions): string // re-exported unchanged from color-value-tools: export { bestTextColor, wcagLevel, contrastRatio, isDark } ``` - All three sampling-based functions (`bestGradientTextColor`, `gradientContrastRatio`, `gradientWcagLevel`) go through a shared internal `sampleGradientContrasts()`: `SAMPLE_STEPS = 9` interior points + the 2 endpoints = **11 total samples** (matches the README's "11-point sampling" claim), each linearly `mixColors(...,{mode:'rgb'})`- interpolated between whichever two flat colors in the input array bracket that sample position. **This treats the input `colors` array as if evenly spaced** — if you pass stops with real explicit CSS `position` percentages, those positions are NOT read here; only the color values matter, uniform spacing is always assumed. - `bestGradientTextColor`'s scoring is always **worst-case** (`Math.min` across all 11 samples) — recommends whichever of black/white has the higher *minimum* contrast, never an average. - **`weightByArea` option removed** (as of this fix) — it existed on both non-legacy overloads (`{detailed?, weightByArea?}`) but was never read anywhere in the implementation; the scoring was always the plain worst-case described above regardless of its value. Removed rather than implemented for real, since the public `colors: string[]` shape carries no per-stop width/position data to weight by — implementing it correctly would need a breaking signature change. If you're looking at an older `dist/`, the option may still be present in the type signature but had zero effect even there. - `createAccessibleGradient`: builds `startColor = adjust(lighten(hex,15))` / `endColor = adjust(hex)`, where `adjust()` tries `step` from 5 to 40 in increments of 5 (lightening if `isDark(textColor)`, else darkening — note this reads `textColor`'s own darkness to decide direction, not the background's), then — only if the single-strategy loop never passed — a second combined lighten+desaturate (or darken+saturate) fallback pass, same step range. If *nothing* passes even that, returns the color **unchanged** — silently may not actually meet `targetLevel`, no error/warning. --- ## 8. CSS variable utilities (`css-variables.ts`) ```ts function extractGradientVariables(gradient: string): string[] // regex: /var\(\s*(--[\w-]+)\s*(?:,[^)]+)?\)/g — dedup via Set, returns names only (no '--' stripped) function resolveGradientVariables(gradient: string, variables: Record): string // per var(...) occurrence: variables[name] ?? inline-fallback.trim() ?? left as 'var(name)' (drops the original inline fallback text if no substitute found and no fallback existed) ``` Pure string/regex operations on an already-built gradient string — no color parsing, no validation that `variables[name]` is actually a valid CSS color. Useful for a build step that bakes a design system's `var(--brand)` references into static values for a context (e.g. an email client, or `gradientToDataURL`) that can't resolve CSS custom properties itself. --- ## 9. Shared helpers (`utils.ts`) ```ts interface ColorStop { color: string; opacity?: number; position?: string | number } function colorStopToString(item: ColorStop): string // opacity===0 → 'transparent' (regardless of color); opacity is a number → hexToRgba (hex) or setAlpha (rgb/hsl/named); position appended as `${str} ${position}` when set interface ResolvedColor { hex: string; isCssVar: boolean; varExpression?: string } function resolveBaseColor(color: string, fallback: string): ResolvedColor ``` `resolveBaseColor()` is the load-bearing helper nearly every generator in this package funnels `baseColor`/`first` through: - `type === 'css-var'` → `{ hex: normalizeHex(fallback), isCssVar: true, varExpression: 'var(name, fallback)' }`. **The `fallback` used for both the computed `hex` AND the emitted `varExpression`'s fallback clause is always the caller's JS-level `fallback` parameter — never the fallback embedded inside the original `var(--x, #123456)` string itself, which is discarded entirely.** Passing `var(--x, #123456)` with the default `fallbackColor: '#f5e477'` produces `var(--x, #f5e477)` in the output, not `var(--x, #123456)` — easy to miss since both are "a fallback color" conceptually but only one of them survives. - `type === 'hex'` → `{ hex: normalizeHex(color), isCssVar: false }`. - otherwise (`rgb`/`hsl`/`named`/anything `normalizeColor` can parse) → `parsed.hex` if present, else falls through to the same `normalizeHex(fallback)` used for the css-var branch. This is what makes this package's own `fallbackColor` convention (pale yellow by default) consistent everywhere *except* the 5 generators fixed in section 6, which bypassed this function and hit `color-value-tools`' own internal black fallback instead before that fix. `toScaleMode()` is an identity function — `ScaleInterpolation` is already restricted to what `createColorScale` accepts, so it exists purely as a type-narrowing no-op, not a real remapping. --- ## 10. Canvas & image export (`canvas-export.ts`) ```ts interface CanvasLinearGradientParams { type:'linear'; stops: Array<{color; offset}>; x0?; y0?; x1?; y1? } interface CanvasRadialGradientParams { type:'radial'; stops: ...; x0?; y0?; r0?; x1?; y1?; r1? } interface CanvasConicGradientParams { type:'conic'; stops: ...; startAngle?: number /* RADIANS, not degrees */; x?; y? } type CanvasGradientParams = CanvasLinearGradientParams | CanvasRadialGradientParams | CanvasConicGradientParams function gradientToCanvasGradient(params: CanvasGradientParams, ctx: CanvasRenderingContext2D): CanvasGradient function gradientToImageData(params: CanvasGradientParams, width: number, height: number): ImageData function gradientToDataURL(params: CanvasGradientParams, width: number, height: number): string ``` - These take flat `{color, offset}` stops (`offset` 0-1) — a completely separate, lower-level shape from `ColorStop` (section 9) used everywhere else in the library; not interchangeable without mapping. - `gradientToImageData`/`gradientToDataURL` create their own canvas internally via a private `createCanvas(width, height)`: prefers `OffscreenCanvas` when the global exists, else a real DOM `` via `document.createElement`, else throws `'[css-magic-gradient] Canvas is not available in this environment...'` (suggests installing the `canvas` npm package for Node/SSR use). - **`getContext()` Web Worker crash — fixed.** The internal helper that picks between `HTMLCanvasElement.getContext` and `OffscreenCanvas.getContext` now checks `typeof HTMLCanvasElement !== 'undefined' && canvas instanceof HTMLCanvasElement` before branching. **Before the fix**, it was a bare `canvas instanceof HTMLCanvasElement` — in a real Web Worker (no DOM at all), `HTMLCanvasElement` is an undeclared global, and `instanceof` against an undeclared identifier throws `ReferenceError` immediately, rather than evaluating to `false` the way you'd expect — meaning `gradientToImageData()`/`gradientToDataURL()` crashed on their very first call inside any Worker, even though `createCanvas()` had correctly produced a usable `OffscreenCanvas` for exactly that environment one line earlier. - **`gradientToDataURL()` OffscreenCanvas crash — fixed.** `OffscreenCanvas` has no `toDataURL()` method (only the async `convertToBlob()`). The function now checks `typeof canvas.toDataURL !== 'function'` first and throws a descriptive `[css-magic-gradient] gradientToDataURL() requires a browser element with toDataURL() support...` error pointing callers at `gradientToImageData()` (which works fine with `OffscreenCanvas`, since `getImageData()` exists on both) or manual `convertToBlob()`. **Before the fix**, this was a bare `TypeError: canvas.toDataURL is not a function` with no guidance — functionally reachable any time `OffscreenCanvas` exists in the current global scope (true in a Worker, but ALSO true in a modern main-thread browser tab, since `createCanvas()` prefers `OffscreenCanvas` whenever the global is defined, not only inside Workers) and the caller specifically asked for a data URL rather than `ImageData`. - `gradientToCanvasGradient()` itself takes an already-existing `ctx` (doesn't call `createCanvas`) — safe to use directly with your own canvas (including a real DOM canvas main-thread-side) without hitting either bug above; only `gradientToImageData`/`gradientToDataURL`'s own internal canvas creation path was affected. - `CanvasConicGradientParams.startAngle` is in **radians**, matching the native `CanvasRenderingContext2D.createConicGradient()` Web API it wraps directly — inconsistent with every angle-like option elsewhere in this package (`angle`, `fromAngle`, `startAngle` in the CSS-string generators) which are all in **degrees**. Easy source of a 57×-off gradient if you reuse a degree value here without converting. --- ## 11. Vue integration (`css-magic-gradient/vue`) 15 reactive hooks, all `(...args: T | Ref) => ComputedRef` — every positional argument independently accepts either a raw value or a `Ref`/`ComputedRef` wrapping one (resolved per-call via an internal `resolve()` helper using `isRef()`), so you can mix reactive and static arguments freely in the same call: ``` useLinearGradient, useMultiStepLinearGradient, useMixedLinearGradient, useRadialGradient, useConicGradient, useRainbowConicGradient, useComplementaryGradient, useTriadicGradient, useAnalogousGradient, useTetradicGradient, useSplitComplementaryGradient, useTintGradient, useShadeGradient, useToneGradient, useAccessibleGradient ``` (Missing `useMonochromaticGradient`/`useHueWheelGradient` — see the gap noted in section 6.) Each hook is a 1:1 `computed(() => createXGradient(resolve(arg1), resolve(arg2), ...))` wrapper — no extra logic, caching, or validation beyond what the underlying `createXGradient` function already does. SSR-safe (pure string computation, no DOM access anywhere in this file). Default export is a Vue plugin: `app.use(cssMagicGradientPlugin)` registers every hook above as `app.config.globalProperties.$useXGradient` (Options API access) — install is optional, the named hook exports work standalone via Composition API without ever calling `app.use()`. --- ## 12. React integration (`css-magic-gradient/react`) The same 15 hooks (identical name list to section 11), each `useMemo(() => createXGradient(...args), [JSON.stringify-based deps])` — plain `string` return (not reactive refs), args are plain values (no `Ref` support/needed, React's own reactivity model doesn't use refs this way). Every hook's dependency array uses `JSON.stringify(options)` for the options object (not a shallow prop-by-prop comparison) — a new object literal passed inline on every render still gets correctly memoized against its *serialized* content, not its identity, avoiding the classic "inline object literal defeats useMemo" trap — but also means options containing functions, `undefined` values, or anything `JSON.stringify` can't faithfully round-trip won't be compared correctly. No default/plugin export (React has no equivalent concept to register). --- ## 13. Fixed-bug history (verify against your installed version) All six fixed via PR #22 (branch `fix/canvas-worker-crash-and-gradient-bugs`), verified by toggling `git stash` between pre-fix and post-fix source and confirming each behavior flips: 1. `getContext()` Web Worker `ReferenceError` crash — section 10. 2. `gradientToDataURL()` `OffscreenCanvas` `TypeError` crash — section 10. 3. `angle: 0` silently ignored in 4 functions in `linear-gradient.ts` — section 2. 4. `createConicGradient({steps:1})` default brightness mode NaN-absorption bug — section 4. 5. 5 harmony/shade generators in `presets.ts` bypassing `resolveBaseColor()`, silently black-fallback on unresolvable input instead of this package's own yellow convention — section 6. 6. Dead, no-op `weightByArea` option removed from `bestGradientTextColor()` — section 7. Also fixed, same PR: the README's Vue example imported `useTetradicGradient`/`useAccessibleGradient` from the package root instead of `css-magic-gradient/vue`. A separate, earlier, already-merged fix (2026-08-31, predates this document and this PR): the root `.` entry used to unconditionally re-export Vue-specific code doing a real `import ... from '@vue/runtime-core'`, breaking every non-Vue consumer even though `vue` is an optional peer. Fixed by moving Vue-specific exports to their own `/vue` subpath (section 11), mirroring the pre-existing `/react` subpath split (section 12) — confirmed via the explanatory comment still present at the top of `src/index.ts`. --- ## 14. Consolidated gotcha list 1. `angle: 0` used to be silently ignored (bare-truthy check) in 4 functions — now fixed via `angle !== undefined`, but if you're generating code defensively against older installs, know that a `0deg` angle historically fell back to `direction` instead (section 2). 2. `createConicGradient`/`createMultiStepLinearGradient`, `steps: 1`: post-fix behavior is a single properly-brightened stop; a pre-fix install instead silently returns the unmodified base color due to `adjustHexBrightness(hex, NaN)` absorbing the divide-by-zero rather than erroring or producing a visible `"NaN"` string (sections 2, 4). 3. `resolveBaseColor()`'s CSS-variable handling discards the fallback color embedded inside the original `var(--x, #123456)` string — only the function's own `fallback` parameter (this package's `fallbackColor` option, default `'#f5e477'`) is used for both the computed working `hex` and the fallback clause re-emitted in the output `var(--x, )` (section 9). 4. `CanvasConicGradientParams.startAngle` is in **radians** (it's a direct pass-through to the native Canvas API); every other angle-like option in the rest of the package (`angle`, `fromAngle`) is in **degrees** (section 10). 5. `gradientToDataURL()` can throw even on the **main thread**, not just in a Worker — `createCanvas()` prefers `OffscreenCanvas` whenever the global exists at all, regardless of thread, so a modern browser tab can hit the "no toDataURL()" path too if `OffscreenCanvas` happens to be what gets created (section 10). 6. No `useMonochromaticGradient`/`useHueWheelGradient` reactive hook in either `/vue` or `/react` — the only 2 of `presets.ts`'s 10 dynamic generators without hook coverage, despite both frameworks otherwise wrapping every other generator in the package 1:1 (sections 6, 11, 12). 7. `RadialHarmonyType` supports 4 harmonies (no `'split-complementary'`) while `presets.ts`'s dedicated harmony generators support 5 (adds `createSplitComplementaryGradient`) — an asymmetry between the two harmony-generation code paths, not a bug, just don't assume parity (section 3). 8. `createTetradicGradient({ type: 'conic' })` builds its own raw `conic-gradient(from 0deg at 50% 50%, ...)` string directly instead of delegating to `createConicGradient()` — `fromAngle`/`position`/ `repeating` are silently not configurable in this one mode/type combination, unlike every other cross-mode delegation in the file (section 6). 9. `createAccessibleGradient()` can silently fail to meet the requested `targetLevel` — if no lightness/saturation step (5 through 40) nor the combined fallback pass achieves the target contrast, the original, non-passing color is returned as-is with no error or warning (section 7). 10. `bestGradientTextColor`/`gradientContrastRatio`/`gradientWcagLevel` all assume their `colors: string[]` input is evenly spaced along the gradient — real CSS `position` percentages on your actual stops are not read by these three functions, only by `colorStopToString`/ the generators that build the CSS string itself (section 7).