css-calipers 0.15.0 → 1.1.0-beta.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.
Files changed (207) hide show
  1. package/LICENSE.txt +1 -1
  2. package/README.md +70 -434
  3. package/dist/cjs/color/formats/display-p3/display-p3.js +25 -0
  4. package/dist/cjs/color/formats/escalate.js +41 -0
  5. package/dist/cjs/color/formats/hex/hex.js +28 -0
  6. package/dist/cjs/color/formats/hexAlpha/hexAlpha.js +25 -0
  7. package/dist/cjs/color/formats/hsl/hsl.js +23 -0
  8. package/dist/cjs/color/formats/hwb/hwb.js +24 -0
  9. package/dist/cjs/color/formats/index.js +36 -0
  10. package/dist/cjs/color/formats/internals.js +93 -0
  11. package/dist/cjs/color/formats/lab/lab.js +24 -0
  12. package/dist/cjs/color/formats/lch/lch.js +23 -0
  13. package/dist/cjs/color/formats/oklab/oklab.js +24 -0
  14. package/dist/cjs/color/formats/oklch/oklch.js +28 -0
  15. package/dist/cjs/color/formats/resolve.js +16 -0
  16. package/dist/cjs/color/formats/rgb/rgb.js +29 -0
  17. package/dist/cjs/color/formats/rgba/rgba.js +27 -0
  18. package/dist/cjs/color/formats/types.js +11 -0
  19. package/dist/cjs/color/index.js +619 -0
  20. package/dist/cjs/color/types.js +2 -0
  21. package/dist/cjs/core.js +11 -8
  22. package/dist/cjs/corpus.js +53 -0
  23. package/dist/cjs/css-values/cssValues.js +78 -0
  24. package/dist/cjs/css-values/index.js +36 -0
  25. package/dist/cjs/css-values/multi.js +290 -0
  26. package/dist/cjs/css-values/propertyMap.js +2 -0
  27. package/dist/cjs/css-values/spec.js +197 -0
  28. package/dist/cjs/default.js +17 -0
  29. package/dist/cjs/factory.js +1 -1
  30. package/dist/cjs/float.js +110 -0
  31. package/dist/cjs/index.js +29 -9
  32. package/dist/cjs/integer.js +114 -0
  33. package/dist/cjs/internal/createCoreApi.js +103 -38
  34. package/dist/cjs/internal/errors.js +13 -14
  35. package/dist/cjs/internal/toPlainDecimal.js +31 -0
  36. package/dist/cjs/measurements.js +41 -0
  37. package/dist/cjs/ratio.js +57 -22
  38. package/dist/cjs/scalar.js +9 -0
  39. package/dist/cjs/units/absolute.js +8 -8
  40. package/dist/cjs/units/angle.js +5 -5
  41. package/dist/cjs/units/container.js +7 -7
  42. package/dist/cjs/units/font-relative.js +13 -13
  43. package/dist/cjs/units/frequency.js +3 -3
  44. package/dist/cjs/units/grid.js +2 -2
  45. package/dist/cjs/units/index.js +9 -9
  46. package/dist/cjs/units/percent.js +4 -4
  47. package/dist/cjs/units/resolution.js +4 -4
  48. package/dist/cjs/units/time.js +3 -3
  49. package/dist/cjs/units/viewport-dynamic.js +7 -7
  50. package/dist/cjs/units/viewport-large.js +7 -7
  51. package/dist/cjs/units/viewport-small.js +7 -7
  52. package/dist/cjs/units/viewport.js +7 -7
  53. package/dist/esm/color/formats/display-p3/display-p3.d.ts +10 -0
  54. package/dist/esm/color/formats/display-p3/display-p3.d.ts.map +1 -0
  55. package/dist/esm/color/formats/display-p3/display-p3.js +22 -0
  56. package/dist/esm/color/formats/escalate.d.ts +10 -0
  57. package/dist/esm/color/formats/escalate.d.ts.map +1 -0
  58. package/dist/esm/color/formats/escalate.js +37 -0
  59. package/dist/esm/color/formats/hex/hex.d.ts +9 -0
  60. package/dist/esm/color/formats/hex/hex.d.ts.map +1 -0
  61. package/dist/esm/color/formats/hex/hex.js +25 -0
  62. package/dist/esm/color/formats/hexAlpha/hexAlpha.d.ts +9 -0
  63. package/dist/esm/color/formats/hexAlpha/hexAlpha.d.ts.map +1 -0
  64. package/dist/esm/color/formats/hexAlpha/hexAlpha.js +22 -0
  65. package/dist/esm/color/formats/hsl/hsl.d.ts +8 -0
  66. package/dist/esm/color/formats/hsl/hsl.d.ts.map +1 -0
  67. package/dist/esm/color/formats/hsl/hsl.js +20 -0
  68. package/dist/esm/color/formats/hwb/hwb.d.ts +9 -0
  69. package/dist/esm/color/formats/hwb/hwb.d.ts.map +1 -0
  70. package/dist/esm/color/formats/hwb/hwb.js +21 -0
  71. package/dist/esm/color/formats/index.d.ts +24 -0
  72. package/dist/esm/color/formats/index.d.ts.map +1 -0
  73. package/dist/esm/color/formats/index.js +32 -0
  74. package/dist/esm/color/formats/internals.d.ts +35 -0
  75. package/dist/esm/color/formats/internals.d.ts.map +1 -0
  76. package/dist/esm/color/formats/internals.js +78 -0
  77. package/dist/esm/color/formats/lab/lab.d.ts +9 -0
  78. package/dist/esm/color/formats/lab/lab.d.ts.map +1 -0
  79. package/dist/esm/color/formats/lab/lab.js +21 -0
  80. package/dist/esm/color/formats/lch/lch.d.ts +8 -0
  81. package/dist/esm/color/formats/lch/lch.d.ts.map +1 -0
  82. package/dist/esm/color/formats/lch/lch.js +20 -0
  83. package/dist/esm/color/formats/oklab/oklab.d.ts +9 -0
  84. package/dist/esm/color/formats/oklab/oklab.d.ts.map +1 -0
  85. package/dist/esm/color/formats/oklab/oklab.js +21 -0
  86. package/dist/esm/color/formats/oklch/oklch.d.ts +13 -0
  87. package/dist/esm/color/formats/oklch/oklch.d.ts.map +1 -0
  88. package/dist/esm/color/formats/oklch/oklch.js +25 -0
  89. package/dist/esm/color/formats/resolve.d.ts +15 -0
  90. package/dist/esm/color/formats/resolve.d.ts.map +1 -0
  91. package/dist/esm/color/formats/resolve.js +12 -0
  92. package/dist/esm/color/formats/rgb/rgb.d.ts +11 -0
  93. package/dist/esm/color/formats/rgb/rgb.d.ts.map +1 -0
  94. package/dist/esm/color/formats/rgb/rgb.js +26 -0
  95. package/dist/esm/color/formats/rgba/rgba.d.ts +9 -0
  96. package/dist/esm/color/formats/rgba/rgba.d.ts.map +1 -0
  97. package/dist/esm/color/formats/rgba/rgba.js +24 -0
  98. package/dist/esm/color/formats/types.d.ts +48 -0
  99. package/dist/esm/color/formats/types.d.ts.map +1 -0
  100. package/dist/esm/color/formats/types.js +7 -0
  101. package/dist/esm/color/index.d.ts +81 -0
  102. package/dist/esm/color/index.d.ts.map +1 -0
  103. package/dist/esm/color/index.js +609 -0
  104. package/dist/esm/color/types.d.ts +294 -0
  105. package/dist/esm/color/types.d.ts.map +1 -0
  106. package/dist/esm/color/types.js +1 -0
  107. package/dist/esm/core.d.ts +76 -270
  108. package/dist/esm/core.d.ts.map +1 -1
  109. package/dist/esm/core.js +5 -7
  110. package/dist/esm/corpus.d.ts +26 -0
  111. package/dist/esm/corpus.d.ts.map +1 -0
  112. package/dist/esm/corpus.js +37 -0
  113. package/dist/esm/css-values/cssValues.d.ts +60 -0
  114. package/dist/esm/css-values/cssValues.d.ts.map +1 -0
  115. package/dist/esm/css-values/cssValues.js +74 -0
  116. package/dist/esm/css-values/index.d.ts +6 -0
  117. package/dist/esm/css-values/index.d.ts.map +1 -0
  118. package/dist/esm/css-values/index.js +12 -0
  119. package/dist/esm/css-values/multi.d.ts +188 -0
  120. package/dist/esm/css-values/multi.d.ts.map +1 -0
  121. package/dist/esm/css-values/multi.js +281 -0
  122. package/dist/esm/css-values/propertyMap.d.ts +39 -0
  123. package/dist/esm/css-values/propertyMap.d.ts.map +1 -0
  124. package/dist/esm/css-values/propertyMap.js +1 -0
  125. package/dist/esm/css-values/spec.d.ts +36 -0
  126. package/dist/esm/css-values/spec.d.ts.map +1 -0
  127. package/dist/esm/css-values/spec.js +194 -0
  128. package/dist/esm/default.d.ts +267 -0
  129. package/dist/esm/default.d.ts.map +1 -0
  130. package/dist/esm/default.js +14 -0
  131. package/dist/esm/factory.d.ts +1 -1
  132. package/dist/esm/factory.d.ts.map +1 -1
  133. package/dist/esm/factory.js +3 -3
  134. package/dist/esm/float.d.ts +35 -0
  135. package/dist/esm/float.d.ts.map +1 -0
  136. package/dist/esm/float.js +104 -0
  137. package/dist/esm/index.d.ts +11 -9
  138. package/dist/esm/index.d.ts.map +1 -1
  139. package/dist/esm/index.js +19 -9
  140. package/dist/esm/integer.d.ts +36 -0
  141. package/dist/esm/integer.d.ts.map +1 -0
  142. package/dist/esm/integer.js +108 -0
  143. package/dist/esm/internal/buildMeasurementCreationError.tests.d.ts +1 -1
  144. package/dist/esm/internal/buildMeasurementCreationError.tests.d.ts.map +1 -1
  145. package/dist/esm/internal/createCoreApi.d.ts +9 -1
  146. package/dist/esm/internal/createCoreApi.d.ts.map +1 -1
  147. package/dist/esm/internal/createCoreApi.js +103 -38
  148. package/dist/esm/internal/createUnitsApi.d.ts +1 -1
  149. package/dist/esm/internal/createUnitsApi.d.ts.map +1 -1
  150. package/dist/esm/internal/errors.d.ts +3 -3
  151. package/dist/esm/internal/errors.d.ts.map +1 -1
  152. package/dist/esm/internal/errors.js +13 -14
  153. package/dist/esm/internal/toPlainDecimal.d.ts +2 -0
  154. package/dist/esm/internal/toPlainDecimal.d.ts.map +1 -0
  155. package/dist/esm/internal/toPlainDecimal.js +27 -0
  156. package/dist/esm/measurements.d.ts +16 -0
  157. package/dist/esm/measurements.d.ts.map +1 -0
  158. package/dist/esm/measurements.js +25 -0
  159. package/dist/esm/ratio.d.ts +14 -5
  160. package/dist/esm/ratio.d.ts.map +1 -1
  161. package/dist/esm/ratio.js +57 -22
  162. package/dist/esm/scalar.d.ts +14 -0
  163. package/dist/esm/scalar.d.ts.map +1 -0
  164. package/dist/esm/scalar.js +5 -0
  165. package/dist/esm/units/absolute.d.ts +1 -1
  166. package/dist/esm/units/absolute.d.ts.map +1 -1
  167. package/dist/esm/units/absolute.js +1 -1
  168. package/dist/esm/units/angle.d.ts +1 -1
  169. package/dist/esm/units/angle.d.ts.map +1 -1
  170. package/dist/esm/units/angle.js +1 -1
  171. package/dist/esm/units/container.d.ts +1 -1
  172. package/dist/esm/units/container.d.ts.map +1 -1
  173. package/dist/esm/units/container.js +1 -1
  174. package/dist/esm/units/font-relative.d.ts +1 -1
  175. package/dist/esm/units/font-relative.d.ts.map +1 -1
  176. package/dist/esm/units/font-relative.js +1 -1
  177. package/dist/esm/units/frequency.d.ts +1 -1
  178. package/dist/esm/units/frequency.d.ts.map +1 -1
  179. package/dist/esm/units/frequency.js +1 -1
  180. package/dist/esm/units/grid.d.ts +1 -1
  181. package/dist/esm/units/grid.d.ts.map +1 -1
  182. package/dist/esm/units/grid.js +1 -1
  183. package/dist/esm/units/index.d.ts +9 -9
  184. package/dist/esm/units/index.d.ts.map +1 -1
  185. package/dist/esm/units/index.js +9 -9
  186. package/dist/esm/units/percent.d.ts +1 -1
  187. package/dist/esm/units/percent.d.ts.map +1 -1
  188. package/dist/esm/units/percent.js +1 -1
  189. package/dist/esm/units/resolution.d.ts +1 -1
  190. package/dist/esm/units/resolution.d.ts.map +1 -1
  191. package/dist/esm/units/resolution.js +1 -1
  192. package/dist/esm/units/time.d.ts +1 -1
  193. package/dist/esm/units/time.d.ts.map +1 -1
  194. package/dist/esm/units/time.js +1 -1
  195. package/dist/esm/units/viewport-dynamic.d.ts +1 -1
  196. package/dist/esm/units/viewport-dynamic.d.ts.map +1 -1
  197. package/dist/esm/units/viewport-dynamic.js +1 -1
  198. package/dist/esm/units/viewport-large.d.ts +1 -1
  199. package/dist/esm/units/viewport-large.d.ts.map +1 -1
  200. package/dist/esm/units/viewport-large.js +1 -1
  201. package/dist/esm/units/viewport-small.d.ts +1 -1
  202. package/dist/esm/units/viewport-small.d.ts.map +1 -1
  203. package/dist/esm/units/viewport-small.js +1 -1
  204. package/dist/esm/units/viewport.d.ts +1 -1
  205. package/dist/esm/units/viewport.d.ts.map +1 -1
  206. package/dist/esm/units/viewport.js +1 -1
  207. package/package.json +55 -13
@@ -0,0 +1,609 @@
1
+ import { blend as blendColors, interpolate, parse, toGamut, wcagContrast, } from 'culori';
2
+ import { colorFormats, defineColorSpace, } from './formats';
3
+ import { chooseFormat } from './formats/escalate';
4
+ import { alphaOf, clamp01, toOklch, violate, } from './formats/internals';
5
+ import { asDescriptor } from './formats/resolve';
6
+ export * from './types';
7
+ /* A resolved color privately carries its store here so it can be re-wrapped by
8
+ * `parseColor` (lib-agnostic: this holds our `Store`, never a culori name). */
9
+ const STORED = Symbol('color.store');
10
+ /* ============================================================================
11
+ * INPUT (Part 1 of the book): parse a `ColorInput` into the canonical store.
12
+ *
13
+ * The store is the boundary between input and the rest of the book. Translatable
14
+ * values become a culori color object; symbolic keywords are kept verbatim and
15
+ * pass through on emit (modifying them throws later, not here). Storage
16
+ * normalization and output formatting are separate, later steps.
17
+ * ==========================================================================*/
18
+ /* The symbolic allowlist, in canonical casing. Runtime mirror of the
19
+ * `Symbolic*` types; `satisfies` keeps each entry a valid keyword. */
20
+ const SYMBOLIC_KEYWORDS = [
21
+ 'currentColor',
22
+ // system colors (current, CSS Color 4)
23
+ 'Canvas',
24
+ 'CanvasText',
25
+ 'LinkText',
26
+ 'VisitedText',
27
+ 'ActiveText',
28
+ 'ButtonFace',
29
+ 'ButtonText',
30
+ 'ButtonBorder',
31
+ 'Field',
32
+ 'FieldText',
33
+ 'Highlight',
34
+ 'HighlightText',
35
+ 'SelectedItem',
36
+ 'SelectedItemText',
37
+ 'Mark',
38
+ 'MarkText',
39
+ 'GrayText',
40
+ 'AccentColor',
41
+ 'AccentColorText',
42
+ // system colors (deprecated, Appendix A - still accepted)
43
+ 'ActiveBorder',
44
+ 'ActiveCaption',
45
+ 'AppWorkspace',
46
+ 'Background',
47
+ 'ButtonHighlight',
48
+ 'ButtonShadow',
49
+ 'CaptionText',
50
+ 'InactiveBorder',
51
+ 'InactiveCaption',
52
+ 'InactiveCaptionText',
53
+ 'InfoBackground',
54
+ 'InfoText',
55
+ 'Menu',
56
+ 'MenuText',
57
+ 'Scrollbar',
58
+ 'ThreeDDarkShadow',
59
+ 'ThreeDFace',
60
+ 'ThreeDHighlight',
61
+ 'ThreeDLightShadow',
62
+ 'ThreeDShadow',
63
+ 'Window',
64
+ 'WindowFrame',
65
+ 'WindowText',
66
+ // CSS-wide cascade keywords
67
+ 'inherit',
68
+ 'initial',
69
+ 'unset',
70
+ 'revert',
71
+ 'revert-layer',
72
+ ];
73
+ /** lower-cased keyword -> canonical casing, for case-insensitive matching. */
74
+ const SYMBOLIC_BY_LOWER = new Map(SYMBOLIC_KEYWORDS.map((keyword) => [
75
+ keyword.toLowerCase(),
76
+ keyword,
77
+ ]));
78
+ const isColorObject = (input) => typeof input === 'object' && input !== null && 'space' in input;
79
+ const isCuloriColor = (input) => typeof input === 'object' && input !== null && 'mode' in input;
80
+ const cloneStore = (store) => store.kind === 'symbolic'
81
+ ? { kind: 'symbolic', keyword: store.keyword }
82
+ : { kind: 'color', color: { ...store.color } };
83
+ /**
84
+ * Adapt a structured `ColorObject` to a culori color object. We accept
85
+ * CSS-authoring ranges (rgb 0-255, hsl/hwb percentages 0-100) and convert them
86
+ * to culori's normalized ranges; lab/lch/oklab/oklch channels are 1:1.
87
+ */
88
+ const colorObjectToCulori = (input) => {
89
+ switch (input.space) {
90
+ case 'rgb': {
91
+ const color = {
92
+ mode: 'rgb',
93
+ r: input.r / 255,
94
+ g: input.g / 255,
95
+ b: input.b / 255,
96
+ };
97
+ if (input.alpha !== undefined)
98
+ color.alpha = input.alpha;
99
+ return color;
100
+ }
101
+ case 'hsl': {
102
+ const color = {
103
+ mode: 'hsl',
104
+ h: input.h,
105
+ s: input.s / 100,
106
+ l: input.l / 100,
107
+ };
108
+ if (input.alpha !== undefined)
109
+ color.alpha = input.alpha;
110
+ return color;
111
+ }
112
+ case 'hwb': {
113
+ const color = {
114
+ mode: 'hwb',
115
+ h: input.h,
116
+ w: input.w / 100,
117
+ b: input.b / 100,
118
+ };
119
+ if (input.alpha !== undefined)
120
+ color.alpha = input.alpha;
121
+ return color;
122
+ }
123
+ case 'lab': {
124
+ const color = {
125
+ mode: 'lab',
126
+ l: input.l,
127
+ a: input.a,
128
+ b: input.b,
129
+ };
130
+ if (input.alpha !== undefined)
131
+ color.alpha = input.alpha;
132
+ return color;
133
+ }
134
+ case 'lch': {
135
+ const color = {
136
+ mode: 'lch',
137
+ l: input.l,
138
+ c: input.c,
139
+ h: input.h,
140
+ };
141
+ if (input.alpha !== undefined)
142
+ color.alpha = input.alpha;
143
+ return color;
144
+ }
145
+ case 'oklab': {
146
+ const color = {
147
+ mode: 'oklab',
148
+ l: input.l,
149
+ a: input.a,
150
+ b: input.b,
151
+ };
152
+ if (input.alpha !== undefined)
153
+ color.alpha = input.alpha;
154
+ return color;
155
+ }
156
+ case 'oklch': {
157
+ const color = {
158
+ mode: 'oklch',
159
+ l: input.l,
160
+ c: input.c,
161
+ h: input.h,
162
+ };
163
+ if (input.alpha !== undefined)
164
+ color.alpha = input.alpha;
165
+ return color;
166
+ }
167
+ }
168
+ };
169
+ /**
170
+ * A plugin-aware string parser: tries the built-in parse FIRST (symbolic keyword,
171
+ * then culori CSS parse), and only if that declines tries each plugin's `parse` in
172
+ * order. Returns a `Store` or `undefined` to decline. Built-in precedence is intact;
173
+ * a plugin only ever claims a string the built-ins reject. The module-level parser is
174
+ * this with no plugins; a factory instance binds its own plugin list.
175
+ */
176
+ const parseString = (input, plugins) => {
177
+ var _a;
178
+ const canonical = SYMBOLIC_BY_LOWER.get(input.toLowerCase());
179
+ if (canonical !== undefined) {
180
+ return { kind: 'symbolic', keyword: canonical };
181
+ }
182
+ const builtIn = parse(input);
183
+ if (builtIn !== undefined) {
184
+ return { kind: 'color', color: builtIn };
185
+ }
186
+ // built-ins declined: offer the string to each plugin's input bridge, in order.
187
+ for (const plugin of plugins) {
188
+ const bridged = (_a = plugin.parse) === null || _a === void 0 ? void 0 : _a.call(plugin, input);
189
+ if (bridged !== undefined) {
190
+ return { kind: 'color', color: bridged };
191
+ }
192
+ }
193
+ return undefined;
194
+ };
195
+ /**
196
+ * Parse any `ColorInput` into the canonical store, optionally consulting custom
197
+ * format plugins for strings the built-ins reject.
198
+ *
199
+ * - string: a symbolic keyword (case-insensitive) -> passthrough store; otherwise
200
+ * parsed as a CSS color; if that declines, each plugin's `parse` is tried; an input
201
+ * no one claims throws.
202
+ * - structured `ColorObject` -> adapted to a culori color.
203
+ * - re-wrap: an existing culori color (internal/engine use) -> reused as-is.
204
+ */
205
+ const parseInput = (input, plugins) => {
206
+ if (typeof input === 'string') {
207
+ const store = parseString(input, plugins);
208
+ if (store === undefined) {
209
+ throw new Error(`color: unparseable color string "${input}"`);
210
+ }
211
+ return store;
212
+ }
213
+ // re-wrap: an existing ResolvedColor carries its store privately.
214
+ if (typeof input === 'object' &&
215
+ input !== null &&
216
+ STORED in input) {
217
+ return cloneStore(input[STORED]);
218
+ }
219
+ if (isColorObject(input)) {
220
+ return { kind: 'color', color: colorObjectToCulori(input) };
221
+ }
222
+ if (isCuloriColor(input)) {
223
+ return { kind: 'color', color: input };
224
+ }
225
+ throw new Error('color: unsupported color input');
226
+ };
227
+ /**
228
+ * Parse any `ColorInput` into the canonical store (built-in formats only). The public,
229
+ * instance-agnostic parser. Factory instances thread their own plugin-aware parser
230
+ * internally; this stays the bare built-in entry point.
231
+ */
232
+ export const parseColor = (input) => parseInput(input, []);
233
+ /* ============================================================================
234
+ * STORAGE (Part 2 of the book): normalize the canonical store.
235
+ *
236
+ * Every translatable color is converted to OKLCH, so the rest of the book works
237
+ * from one perceptually-uniform representation: modifications become direct
238
+ * coordinate edits (l/c/h), and outputs convert out of OKLCH. Symbolic keywords
239
+ * have no value to normalize and pass through untouched.
240
+ *
241
+ * This is pure culori math and runs anywhere JS runs - it needs no browser
242
+ * `oklch()` support. Browser compatibility is purely an output-step concern
243
+ * (which format you emit), not a storage concern.
244
+ * ==========================================================================*/
245
+ /** Normalize a parsed store into the canonical OKLCH working space. */
246
+ export const storeColor = (store) => {
247
+ if (store.kind === 'symbolic') {
248
+ return store;
249
+ }
250
+ return { kind: 'color', color: toOklch(store.color) };
251
+ };
252
+ /* ============================================================================
253
+ * OUTPUT (Part 3 of the book): render the store in any format; build the result.
254
+ *
255
+ * The store is OKLCH; output converts OUT of it with culori and serializes. Every
256
+ * alpha-capable format always renders its alpha slot; `rgb`/`hex` carry no alpha.
257
+ * "Can't faithfully represent this" cases (dropped alpha, out-of-gamut) are
258
+ * surfaced via the strictness knob (throw in dev / warn in prod by default), with a
259
+ * best-effort value (clamped chroma) still produced in the warn case.
260
+ * ==========================================================================*/
261
+ // The unified format registry (each entry a descriptor: render + metadata) lives in
262
+ // `./formats`; re-export it (plus the descriptor authoring surface) so `colorFormats.hex`
263
+ // stays the public output selector and custom formats can be defined.
264
+ export { colorFormats, defineColorSpace, };
265
+ // Render a (non-fully-transparent) color in the requested format by dispatching to
266
+ // that format's descriptor. The descriptors own the per-format render; this single
267
+ // dispatch replaced the old per-format switch.
268
+ const serialize = (color, format, cfg) => asDescriptor(format, colorFormats).render(color, cfg);
269
+ const renderColor = (color, format, cfg) => {
270
+ // fully-transparent rendering policy (alpha 0): a keyword, or white/black at 0.
271
+ if (alphaOf(color) === 0) {
272
+ if (cfg.transparent === 'keyword')
273
+ return 'transparent';
274
+ // `preserve`: keep the colour's own RGB at alpha 0 (the truest fade), i.e.
275
+ // render the stored colour as-is, exactly like a non-transparent value.
276
+ if (cfg.transparent === 'preserve') {
277
+ return serialize(color, format, cfg);
278
+ }
279
+ const substitute = cfg.transparent === 'white'
280
+ ? { mode: 'rgb', r: 1, g: 1, b: 1, alpha: 0 }
281
+ : { mode: 'rgb', r: 0, g: 0, b: 0, alpha: 0 };
282
+ return serialize(substitute, format, cfg);
283
+ }
284
+ return serialize(color, format, cfg);
285
+ };
286
+ const render = (store, output, cfg) => {
287
+ // symbolic colors pass through: their keyword emits for any requested format.
288
+ if (store.kind === 'symbolic') {
289
+ return store.keyword;
290
+ }
291
+ // resolve the output (a single format, or a priority list) to one faithful format.
292
+ const format = chooseFormat(store.color, output);
293
+ return renderColor(store.color, format, cfg);
294
+ };
295
+ const wrapHue = (h) => ((h % 360) + 360) % 360;
296
+ /* Map a colour into the sRGB gamut (a no-op for in-gamut colours). Separable blend
297
+ * modes are sRGB-defined, so a wide-gamut operand is mapped here before blending; an
298
+ * in-gamut colour passes through losslessly. */
299
+ const toSrgbGamut = toGamut('rgb', 'oklch');
300
+ const builtInBinding = {
301
+ parse: parseColor,
302
+ plugins: [],
303
+ };
304
+ const resolveWith = (store, cfg, binding) => {
305
+ // a format selector: same color, new configured output, still finished via .css().
306
+ const withFormat = (output) => resolveWith(store, { ...cfg, output }, binding);
307
+ // a transparency selector: same colour and format, new alpha-0 render policy.
308
+ const withTransparent = (transparent) => resolveWith(store, { ...cfg, transparent }, binding);
309
+ // a modification: a NEW color (re-normalized to OKLCH), same config.
310
+ const withColor = (raw) => resolveWith(storeColor({ kind: 'color', color: raw }), cfg, binding);
311
+ const self = () => resolveWith(store, cfg, binding);
312
+ // modifications are only valid on a translatable color; on a symbolic keyword
313
+ // they are a violation (throw in dev / warn in prod) and leave the color as-is.
314
+ const modifiable = (op) => {
315
+ if (store.kind === 'symbolic') {
316
+ violate(`color: cannot ${op} a symbolic color '${store.keyword}'`, cfg.strictness);
317
+ return undefined;
318
+ }
319
+ return store.color;
320
+ };
321
+ const targetColor = (target) => {
322
+ const resolved = storeColor(binding.parse(target));
323
+ if (resolved.kind !== 'color') {
324
+ violate('color: cannot mix with a symbolic color', cfg.strictness);
325
+ return undefined;
326
+ }
327
+ return resolved.color;
328
+ };
329
+ const blend = (op, target, ratio, mode, makeBaseSolid) => {
330
+ const c = modifiable(op);
331
+ if (c === undefined)
332
+ return undefined;
333
+ const t = targetColor(target);
334
+ if (t === undefined)
335
+ return undefined;
336
+ const base = makeBaseSolid ? { ...c, alpha: 1 } : c;
337
+ return interpolate([
338
+ base,
339
+ t,
340
+ ], mode)(clamp01(ratio));
341
+ };
342
+ // a separable blend: compose `target` onto this colour with a sRGB-defined blend
343
+ // mode. Both operands are gamut-mapped into sRGB (a no-op for in-gamut colours) and
344
+ // blended there; the result is re-normalized to OKLCH by `withColor`.
345
+ const applyBlend = (op, target, mode) => {
346
+ const c = modifiable(op);
347
+ if (c === undefined)
348
+ return undefined;
349
+ const t = targetColor(target);
350
+ if (t === undefined)
351
+ return undefined;
352
+ return blendColors([
353
+ toSrgbGamut(c),
354
+ toSrgbGamut(t),
355
+ ], mode, 'rgb');
356
+ };
357
+ // adjust this colour's OKLCH lightness toward black/white to reach a WCAG contrast
358
+ // ratio against `target`, moving the least amount needed. Used by `ensureContrast`.
359
+ const liftToContrast = (base, target, ratio) => {
360
+ const at = (l) => wcagContrast({ ...base, l }, target);
361
+ // already sufficient: leave it.
362
+ if (at(base.l) >= ratio)
363
+ return base;
364
+ // pick the endpoint (black L=0 or white L=1) that yields the most contrast.
365
+ const endpoint = at(1) >= at(0) ? 1 : 0;
366
+ // unreachable even at the endpoint: return the best-achievable colour.
367
+ if (at(endpoint) < ratio)
368
+ return { ...base, l: endpoint };
369
+ // contrast is monotonic between base.l and the endpoint: binary-search the
370
+ // smallest move that still meets the ratio. `lo` stays insufficient, `hi`
371
+ // sufficient, regardless of numeric direction.
372
+ let lo = base.l;
373
+ let hi = endpoint;
374
+ for (let i = 0; i < 40; i += 1) {
375
+ const mid = (lo + hi) / 2;
376
+ if (at(mid) >= ratio)
377
+ hi = mid;
378
+ else
379
+ lo = mid;
380
+ }
381
+ return { ...base, l: hi };
382
+ };
383
+ // a format override selector: same colour, new configured output (a single
384
+ // format or a priority list), still finished via .css(). Mirrors `withFormat`
385
+ // and `withTransparent`; the output threads straight into `cfg.output`.
386
+ const formatAs = (formats) => resolveWith(store, { ...cfg, output: formats }, binding);
387
+ const result = {
388
+ css: () => render(store, cfg.output, cfg),
389
+ formatAs,
390
+ rgba: () => withFormat(colorFormats.rgba),
391
+ rgb: () => withFormat(colorFormats.rgb),
392
+ hex: () => withFormat(colorFormats.hex),
393
+ hexAlpha: () => withFormat(colorFormats.hexAlpha),
394
+ hsl: () => withFormat(colorFormats.hsl),
395
+ hwb: () => withFormat(colorFormats.hwb),
396
+ lab: () => withFormat(colorFormats.lab),
397
+ lch: () => withFormat(colorFormats.lch),
398
+ oklab: () => withFormat(colorFormats.oklab),
399
+ oklch: () => withFormat(colorFormats.oklch),
400
+ displayP3: () => withFormat(colorFormats.displayP3),
401
+ transparentAs: (mode) => withTransparent(mode),
402
+ alpha: ((value) => {
403
+ var _a;
404
+ if (value === undefined) {
405
+ return store.kind === 'color' ? ((_a = store.color.alpha) !== null && _a !== void 0 ? _a : 1) : 1;
406
+ }
407
+ const c = modifiable('set the alpha of');
408
+ return c === undefined
409
+ ? self()
410
+ : withColor({ ...c, alpha: value });
411
+ }),
412
+ darken: (amount = 0.1) => {
413
+ const c = modifiable('darken');
414
+ return c === undefined
415
+ ? self()
416
+ : withColor({ ...c, l: c.l * (1 - clamp01(amount)) });
417
+ },
418
+ lighten: (amount = 0.1) => {
419
+ const c = modifiable('lighten');
420
+ return c === undefined
421
+ ? self()
422
+ : withColor({ ...c, l: c.l + (1 - c.l) * clamp01(amount) });
423
+ },
424
+ brighten: (amount = 0.1) => result.lighten(amount),
425
+ saturate: (amount = 0.1) => {
426
+ const c = modifiable('saturate');
427
+ return c === undefined
428
+ ? self()
429
+ : withColor({ ...c, c: c.c * (1 + Math.max(0, amount)) });
430
+ },
431
+ desaturate: (amount = 0.1) => {
432
+ const c = modifiable('desaturate');
433
+ return c === undefined
434
+ ? self()
435
+ : withColor({ ...c, c: c.c * (1 - clamp01(amount)) });
436
+ },
437
+ hueShift: (value) => {
438
+ var _a;
439
+ const c = modifiable('hue-shift');
440
+ return c === undefined
441
+ ? self()
442
+ : withColor({
443
+ ...c,
444
+ h: wrapHue(((_a = c.h) !== null && _a !== void 0 ? _a : 0) + value.getValue()),
445
+ });
446
+ },
447
+ setLightness: (value) => {
448
+ const c = modifiable('set the lightness of');
449
+ return c === undefined ? self() : withColor({ ...c, l: value });
450
+ },
451
+ setChroma: (value) => {
452
+ const c = modifiable('set the chroma of');
453
+ return c === undefined ? self() : withColor({ ...c, c: value });
454
+ },
455
+ setHue: (value) => {
456
+ const c = modifiable('set the hue of');
457
+ return c === undefined
458
+ ? self()
459
+ : withColor({ ...c, h: wrapHue(value.getValue()) });
460
+ },
461
+ complement: () => {
462
+ var _a;
463
+ const c = modifiable('complement');
464
+ return c === undefined
465
+ ? self()
466
+ : withColor({ ...c, h: wrapHue(((_a = c.h) !== null && _a !== void 0 ? _a : 0) + 180) });
467
+ },
468
+ invert: (amount = 1) => {
469
+ const c = modifiable('invert');
470
+ if (c === undefined)
471
+ return self();
472
+ const a = clamp01(amount);
473
+ // L + ((1 - L) - L) * a; a=1 -> 1 - L, a=0.5 -> 0.5 for any L.
474
+ return withColor({ ...c, l: c.l + (1 - 2 * c.l) * a });
475
+ },
476
+ grayscale: (amount = 1) => {
477
+ const c = modifiable('grayscale');
478
+ return c === undefined
479
+ ? self()
480
+ : withColor({ ...c, c: c.c * (1 - clamp01(amount)) });
481
+ },
482
+ blend: (other, mode) => {
483
+ const blended = applyBlend('blend', other, mode);
484
+ return blended === undefined ? self() : withColor(blended);
485
+ },
486
+ ensureContrast: (other, ratio = 4.5) => {
487
+ const c = modifiable('ensure the contrast of');
488
+ if (c === undefined)
489
+ return self();
490
+ const t = targetColor(other);
491
+ if (t === undefined)
492
+ return self();
493
+ return withColor(liftToContrast(c, t, ratio));
494
+ },
495
+ contrast: (other) => {
496
+ const c = modifiable('measure the contrast of');
497
+ if (c === undefined)
498
+ return Number.NaN;
499
+ const t = targetColor(other);
500
+ if (t === undefined)
501
+ return Number.NaN;
502
+ return wcagContrast(c, t);
503
+ },
504
+ mix: (target, ratio = 0.5, mode = 'oklch') => {
505
+ const mixed = blend('mix', target, ratio, mode, false);
506
+ return mixed === undefined ? self() : withColor(mixed);
507
+ },
508
+ mixSolid: (target, ratio = 0.5, mode = 'oklch') => {
509
+ const mixed = blend('mix', target, ratio, mode, true);
510
+ return mixed === undefined ? self() : withColor(mixed);
511
+ },
512
+ mixWithAlpha: (target, ratio = 0.5, alpha, mode = 'oklch') => {
513
+ var _a;
514
+ const c = modifiable('mix');
515
+ if (c === undefined)
516
+ return self();
517
+ const mixed = blend('mix', target, ratio, mode, true);
518
+ if (mixed === undefined)
519
+ return self();
520
+ return withColor({ ...mixed, alpha: (_a = alpha !== null && alpha !== void 0 ? alpha : c.alpha) !== null && _a !== void 0 ? _a : 1 });
521
+ },
522
+ solid: () => {
523
+ const c = modifiable('solidify');
524
+ return c === undefined ? self() : withColor({ ...c, alpha: 1 });
525
+ },
526
+ clone: () => self(),
527
+ };
528
+ // carry the store privately so this result can be re-wrapped via `color(result)`.
529
+ Object.defineProperty(result, STORED, { value: store });
530
+ // Named custom-format selectors: one lazy getter per registered plugin (e.g.
531
+ // `.zoo`), mirroring the built-in format selectors and the `transparentAs` chain.
532
+ // Lazy (a getter, not an eager property) so a child result is built only on access,
533
+ // avoiding eager construction recursion. Each returns this same colour reconfigured
534
+ // to render through that plugin's descriptor.
535
+ for (const plugin of binding.plugins) {
536
+ Object.defineProperty(result, plugin.format, {
537
+ enumerable: false,
538
+ get: () => withFormat(plugin),
539
+ });
540
+ }
541
+ return result;
542
+ };
543
+ /**
544
+ * Resolve a store + config into an immutable, navigable `ResolvedColor`, using the
545
+ * built-in parser and no custom plugins. The public, instance-agnostic resolver
546
+ * (re-exported as `resolveColor`). Factory instances use `resolveWith` internally to
547
+ * thread their own plugin-aware parser and named selectors.
548
+ */
549
+ export const resolve = (store, cfg) => resolveWith(store, cfg, builtInBinding);
550
+ /**
551
+ * The default output priority: the simplest faithful format first. With no argument
552
+ * `.css()` escalates down this ladder to the first format that holds the color (see
553
+ * `formats/README.md`). Overridable per book via `publishBookColor({ config })`.
554
+ *
555
+ * Pared to one format per distinct output space, most popular first (see
556
+ * `docs/color-format-popularity.md`). Every other built-in format is a redundant
557
+ * cover of one of these three, so it would never win escalation; it stays reachable
558
+ * via a named selector (`.hwb()`, ...) or `.formatAs(colorFormats.x)`.
559
+ * - `hex`: solids (opaque sRGB). ~76% of real-world colour declarations.
560
+ * - `rgba`: translucent sRGB. The dominant alpha form (rgba >> #rrggbbaa, hsla).
561
+ * - `oklch`: the unbounded floor, holds any colour (P3 and beyond), and is the
562
+ * primitive's own storage space, so it is the zero-loss fallback.
563
+ */
564
+ export const defaultFormatPriority = [
565
+ colorFormats.hex,
566
+ colorFormats.rgba,
567
+ colorFormats.oklch,
568
+ ];
569
+ /** The colour primitive's defaults. */
570
+ export const defaultColorConfig = {
571
+ output: defaultFormatPriority,
572
+ strictness: 'auto',
573
+ transparent: 'keyword',
574
+ omitOpaqueAlpha: false,
575
+ };
576
+ /**
577
+ * Build a `color()`-shaped function bound to the given custom format plugins. The
578
+ * instance parser tries the built-in parse first, then each plugin's `parse` in order;
579
+ * the plugins are valid `output` descriptors and resolve through `asDescriptor`. The
580
+ * returned result exposes a typed lazy selector per plugin format.
581
+ */
582
+ export const createColor = (config) => {
583
+ const plugins = config.formats;
584
+ // the per-instance registry: built-ins, then plugins (a plugin may shadow a built-in
585
+ // name; built-in parse precedence is unaffected, it is consulted before plugins).
586
+ const registry = {
587
+ ...colorFormats,
588
+ };
589
+ for (const plugin of plugins) {
590
+ registry[plugin.format] = plugin;
591
+ }
592
+ // the plugin-aware parser threaded through every parse this instance does.
593
+ const instanceParse = (input) => parseInput(input, plugins);
594
+ const binding = { parse: instanceParse, plugins };
595
+ const instance = (input, callConfig) => resolveWith(storeColor(instanceParse(input)), callConfig === undefined
596
+ ? defaultColorConfig
597
+ : { ...defaultColorConfig, ...callConfig }, binding);
598
+ return Object.assign(instance, { formats: registry });
599
+ };
600
+ /**
601
+ * `color(input, config?)`: the native colour input, alongside `m()`, `r()`, `i()`,
602
+ * `f()`. Parses + normalizes `input` to OKLCH and resolves it with the default
603
+ * config (override per call via `config`). Mirrors how `m()` wraps a measurement.
604
+ *
605
+ * It is `createColor({ formats: [] })` at defaults: the module-level instance is just
606
+ * the factory with no custom plugins, so the default and the factory share one
607
+ * construction path. Typed as the bare `(input, config?) => ResolvedColor` surface.
608
+ */
609
+ export const color = createColor({ formats: [] });