@fusion.dev/helix-core 0.57.0 → 0.58.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/custom-elements.json +224 -11
- package/dist/controllers/token-color-controller/token-color-controller.d.ts +3 -3
- package/dist/controllers/token-color-controller/token-color-controller.js +9 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/utils/css-color/css-color.d.ts +76 -0
- package/dist/utils/css-color/css-color.js +168 -0
- package/dist/utils/read-color/read-color.d.ts +36 -0
- package/dist/utils/read-color/read-color.js +36 -0
- package/package.json +4 -4
package/custom-elements.json
CHANGED
|
@@ -2095,19 +2095,9 @@
|
|
|
2095
2095
|
"declarations": [
|
|
2096
2096
|
{
|
|
2097
2097
|
"kind": "class",
|
|
2098
|
-
"description": "Reads a design token as the sRGB triple a shader or a 2D canvas can take, and\ntells the host when the palette underneath it moves.\n\n**Two hops, and both are load-bearing.** A custom property's own value is the\nunevaluated function text, so `getComputedStyle` on a probe is the only way\nto see what it resolves to.
|
|
2098
|
+
"description": "Reads a design token as the sRGB triple a shader or a 2D canvas can take, and\ntells the host when the palette underneath it moves.\n\n**Two hops, and both are load-bearing.** A custom property's own value is the\nunevaluated function text, so `getComputedStyle` on a probe is the only way\nto see what it resolves to. The second hop is `readColor`, which is shared\nwith every other caller that has a resolved color and needs channels: what it\nresolves to is not `rgb()`, the\npalette derives from an `oklch()` seed, and Chrome answers `lab(100 0 0)` for\nwhite. Parsing that with a regular expression for numbers reads the three\nchannels as 100, 0 and 0, which divided by 255 is a dark red.\n\nA screenshot cannot catch that: a warm cast on a saturated gradient is also\nwhat chroma subsampling produces, so the picture agrees with the bug.\n\nThe conversion is therefore the browser's own: fill a pixel and read it back.\nThat holds for `lab()`, `oklch()`, `color-mix()` and whatever a token is\nwritten in next, which is the property worth having in a design system whose\npalette is derived rather than listed.\n\n**A controller and not a mixin**, by this repository's own line: it adds no\nproperty or method a consumer sets. It reacts to lifecycle, holding a probe\nthat has to be created after the host connects and removed when it leaves,\nand it watches for the color mode changing so the host can repaint.\n\n**Client only, and that is the contract rather than a limitation.** There is\nno computed style on a server and no canvas to convert with, so `read`\nanswers the fallback there. Every caller is drawing into a canvas, which does\nnot exist on a server either.",
|
|
2099
2099
|
"name": "TokenColorController",
|
|
2100
2100
|
"members": [
|
|
2101
|
-
{
|
|
2102
|
-
"kind": "field",
|
|
2103
|
-
"name": "context",
|
|
2104
|
-
"type": {
|
|
2105
|
-
"text": "CanvasRenderingContext2D | null"
|
|
2106
|
-
},
|
|
2107
|
-
"privacy": "private",
|
|
2108
|
-
"default": "null",
|
|
2109
|
-
"description": "Where a resolved color is converted, one pixel at a time."
|
|
2110
|
-
},
|
|
2111
2101
|
{
|
|
2112
2102
|
"kind": "field",
|
|
2113
2103
|
"name": "modes",
|
|
@@ -5977,6 +5967,54 @@
|
|
|
5977
5967
|
"module": "./utils/create-sheet/create-sheet.js"
|
|
5978
5968
|
}
|
|
5979
5969
|
},
|
|
5970
|
+
{
|
|
5971
|
+
"kind": "js",
|
|
5972
|
+
"name": "Color",
|
|
5973
|
+
"declaration": {
|
|
5974
|
+
"name": "Color",
|
|
5975
|
+
"module": "./utils/css-color/css-color.js"
|
|
5976
|
+
}
|
|
5977
|
+
},
|
|
5978
|
+
{
|
|
5979
|
+
"kind": "js",
|
|
5980
|
+
"name": "ColorFormat",
|
|
5981
|
+
"declaration": {
|
|
5982
|
+
"name": "ColorFormat",
|
|
5983
|
+
"module": "./utils/css-color/css-color.js"
|
|
5984
|
+
}
|
|
5985
|
+
},
|
|
5986
|
+
{
|
|
5987
|
+
"kind": "js",
|
|
5988
|
+
"name": "formatColor",
|
|
5989
|
+
"declaration": {
|
|
5990
|
+
"name": "formatColor",
|
|
5991
|
+
"module": "./utils/css-color/css-color.js"
|
|
5992
|
+
}
|
|
5993
|
+
},
|
|
5994
|
+
{
|
|
5995
|
+
"kind": "js",
|
|
5996
|
+
"name": "parseColor",
|
|
5997
|
+
"declaration": {
|
|
5998
|
+
"name": "parseColor",
|
|
5999
|
+
"module": "./utils/css-color/css-color.js"
|
|
6000
|
+
}
|
|
6001
|
+
},
|
|
6002
|
+
{
|
|
6003
|
+
"kind": "js",
|
|
6004
|
+
"name": "splitColors",
|
|
6005
|
+
"declaration": {
|
|
6006
|
+
"name": "splitColors",
|
|
6007
|
+
"module": "./utils/css-color/css-color.js"
|
|
6008
|
+
}
|
|
6009
|
+
},
|
|
6010
|
+
{
|
|
6011
|
+
"kind": "js",
|
|
6012
|
+
"name": "toCss",
|
|
6013
|
+
"declaration": {
|
|
6014
|
+
"name": "toCss",
|
|
6015
|
+
"module": "./utils/css-color/css-color.js"
|
|
6016
|
+
}
|
|
6017
|
+
},
|
|
5980
6018
|
{
|
|
5981
6019
|
"kind": "js",
|
|
5982
6020
|
"name": "customElement",
|
|
@@ -6193,6 +6231,22 @@
|
|
|
6193
6231
|
"module": "./utils/on-unless-false/on-unless-false.js"
|
|
6194
6232
|
}
|
|
6195
6233
|
},
|
|
6234
|
+
{
|
|
6235
|
+
"kind": "js",
|
|
6236
|
+
"name": "ColorBytes",
|
|
6237
|
+
"declaration": {
|
|
6238
|
+
"name": "ColorBytes",
|
|
6239
|
+
"module": "./utils/read-color/read-color.js"
|
|
6240
|
+
}
|
|
6241
|
+
},
|
|
6242
|
+
{
|
|
6243
|
+
"kind": "js",
|
|
6244
|
+
"name": "readColor",
|
|
6245
|
+
"declaration": {
|
|
6246
|
+
"name": "readColor",
|
|
6247
|
+
"module": "./utils/read-color/read-color.js"
|
|
6248
|
+
}
|
|
6249
|
+
},
|
|
6196
6250
|
{
|
|
6197
6251
|
"kind": "js",
|
|
6198
6252
|
"name": "Row",
|
|
@@ -9803,6 +9857,130 @@
|
|
|
9803
9857
|
}
|
|
9804
9858
|
]
|
|
9805
9859
|
},
|
|
9860
|
+
{
|
|
9861
|
+
"kind": "javascript-module",
|
|
9862
|
+
"path": "src/utils/css-color/css-color.ts",
|
|
9863
|
+
"declarations": [
|
|
9864
|
+
{
|
|
9865
|
+
"kind": "function",
|
|
9866
|
+
"name": "splitColors",
|
|
9867
|
+
"return": {
|
|
9868
|
+
"type": {
|
|
9869
|
+
"text": "string[]"
|
|
9870
|
+
}
|
|
9871
|
+
},
|
|
9872
|
+
"parameters": [
|
|
9873
|
+
{
|
|
9874
|
+
"name": "value",
|
|
9875
|
+
"type": {
|
|
9876
|
+
"text": "string"
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
],
|
|
9880
|
+
"description": "Splits a comma-separated list of colors, ignoring the commas inside them.\n\nA plain `split(',')` cannot hold the legacy syntaxes:\n`#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,\nfour of them not colors, and the row quietly renders three swatches instead of\nfour. The modern space-separated forms survive it, which is what makes such a\nlist look like it works.\n\nDepth rather than a regular expression, because the thing that matters is\nwhether a comma is inside a function call and that is exactly what a counter\nanswers. No color syntax nests, so a single counter is enough."
|
|
9881
|
+
},
|
|
9882
|
+
{
|
|
9883
|
+
"kind": "function",
|
|
9884
|
+
"name": "parseColor",
|
|
9885
|
+
"return": {
|
|
9886
|
+
"type": {
|
|
9887
|
+
"text": "Color | null"
|
|
9888
|
+
}
|
|
9889
|
+
},
|
|
9890
|
+
"parameters": [
|
|
9891
|
+
{
|
|
9892
|
+
"name": "value",
|
|
9893
|
+
"type": {
|
|
9894
|
+
"text": "string"
|
|
9895
|
+
}
|
|
9896
|
+
}
|
|
9897
|
+
],
|
|
9898
|
+
"description": "Any of the syntaxes to the working model, or `null` where it is not a color.\n\nNamed colors are not here. Resolving `rebeccapurple` means either shipping\nthe table of a hundred and forty or asking the browser, and the browser is not\npresent on the server: a field that accepted a name client-side and rejected\nit during a server render would be worse than one that never took it."
|
|
9899
|
+
},
|
|
9900
|
+
{
|
|
9901
|
+
"kind": "function",
|
|
9902
|
+
"name": "formatColor",
|
|
9903
|
+
"return": {
|
|
9904
|
+
"type": {
|
|
9905
|
+
"text": "string"
|
|
9906
|
+
}
|
|
9907
|
+
},
|
|
9908
|
+
"parameters": [
|
|
9909
|
+
{
|
|
9910
|
+
"name": "color",
|
|
9911
|
+
"type": {
|
|
9912
|
+
"text": "Color"
|
|
9913
|
+
}
|
|
9914
|
+
},
|
|
9915
|
+
{
|
|
9916
|
+
"name": "format",
|
|
9917
|
+
"type": {
|
|
9918
|
+
"text": "ColorFormat"
|
|
9919
|
+
}
|
|
9920
|
+
},
|
|
9921
|
+
{
|
|
9922
|
+
"name": "withAlpha",
|
|
9923
|
+
"type": {
|
|
9924
|
+
"text": "boolean"
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
],
|
|
9928
|
+
"description": "The working model back out, in the syntax asked for.\n\nThe alpha is written only where the component carries one. A consumer who did\nnot ask for transparency should not have to strip `ff` off every hex or read\npast a fourth argument they never wanted."
|
|
9929
|
+
},
|
|
9930
|
+
{
|
|
9931
|
+
"kind": "function",
|
|
9932
|
+
"name": "toCss",
|
|
9933
|
+
"return": {
|
|
9934
|
+
"type": {
|
|
9935
|
+
"text": "string"
|
|
9936
|
+
}
|
|
9937
|
+
},
|
|
9938
|
+
"parameters": [
|
|
9939
|
+
{
|
|
9940
|
+
"name": "color",
|
|
9941
|
+
"type": {
|
|
9942
|
+
"text": "Color"
|
|
9943
|
+
}
|
|
9944
|
+
}
|
|
9945
|
+
],
|
|
9946
|
+
"description": "The color as CSS, for the swatches and the tracks. Always with its alpha."
|
|
9947
|
+
}
|
|
9948
|
+
],
|
|
9949
|
+
"exports": [
|
|
9950
|
+
{
|
|
9951
|
+
"kind": "js",
|
|
9952
|
+
"name": "splitColors",
|
|
9953
|
+
"declaration": {
|
|
9954
|
+
"name": "splitColors",
|
|
9955
|
+
"module": "src/utils/css-color/css-color.ts"
|
|
9956
|
+
}
|
|
9957
|
+
},
|
|
9958
|
+
{
|
|
9959
|
+
"kind": "js",
|
|
9960
|
+
"name": "parseColor",
|
|
9961
|
+
"declaration": {
|
|
9962
|
+
"name": "parseColor",
|
|
9963
|
+
"module": "src/utils/css-color/css-color.ts"
|
|
9964
|
+
}
|
|
9965
|
+
},
|
|
9966
|
+
{
|
|
9967
|
+
"kind": "js",
|
|
9968
|
+
"name": "formatColor",
|
|
9969
|
+
"declaration": {
|
|
9970
|
+
"name": "formatColor",
|
|
9971
|
+
"module": "src/utils/css-color/css-color.ts"
|
|
9972
|
+
}
|
|
9973
|
+
},
|
|
9974
|
+
{
|
|
9975
|
+
"kind": "js",
|
|
9976
|
+
"name": "toCss",
|
|
9977
|
+
"declaration": {
|
|
9978
|
+
"name": "toCss",
|
|
9979
|
+
"module": "src/utils/css-color/css-color.ts"
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9982
|
+
]
|
|
9983
|
+
},
|
|
9806
9984
|
{
|
|
9807
9985
|
"kind": "javascript-module",
|
|
9808
9986
|
"path": "src/utils/custom-element/custom-element.ts",
|
|
@@ -10474,6 +10652,41 @@
|
|
|
10474
10652
|
}
|
|
10475
10653
|
]
|
|
10476
10654
|
},
|
|
10655
|
+
{
|
|
10656
|
+
"kind": "javascript-module",
|
|
10657
|
+
"path": "src/utils/read-color/read-color.ts",
|
|
10658
|
+
"declarations": [
|
|
10659
|
+
{
|
|
10660
|
+
"kind": "function",
|
|
10661
|
+
"name": "readColor",
|
|
10662
|
+
"return": {
|
|
10663
|
+
"type": {
|
|
10664
|
+
"text": "ColorBytes | undefined"
|
|
10665
|
+
}
|
|
10666
|
+
},
|
|
10667
|
+
"parameters": [
|
|
10668
|
+
{
|
|
10669
|
+
"name": "value",
|
|
10670
|
+
"type": {
|
|
10671
|
+
"text": "string"
|
|
10672
|
+
},
|
|
10673
|
+
"description": "A resolved color, as `getComputedStyle` reports one."
|
|
10674
|
+
}
|
|
10675
|
+
],
|
|
10676
|
+
"description": "A computed color as three sRGB bytes, or nothing where it cannot be read.\n\n**Painting it is the only way to read it.** A color-typed property is\nresolved before it is reported, but resolved is not the same as converted:\nChrome hands back `lab(100 0 0)` for a token built through `oklab`, and\nscraping the first three numbers out of that and calling them red, green and\nblue made white arrive as `rgb(100, 0, 0)`. In light mode the near dots were\n`lab(0 0 0)`, which scrapes to black and looks right, so only dark mode\nshowed it: dark red mixed toward black, on a near-black ground.\n\n**Painted twice, over two different grounds, which is what makes the refusal\nhonest.** Assigning a value the engine will not parse leaves `fillStyle`\nexactly where it was, so a single paint cannot tell a color apart from the\nground it failed to replace. Over black and over white, a value that parsed\ngives the same answer twice and one that did not gives two. A caller then\nchooses its own fallback rather than being handed a color nobody picked,\nwhich is the half both earlier copies of this had to guess at.\n\nA readback is not free, so the answer is held against the string it came\nfrom, refusals included. The oldest goes when the store is full rather than\nall of them, or a page deriving colors on a slider pays for every frame\ntwice."
|
|
10677
|
+
}
|
|
10678
|
+
],
|
|
10679
|
+
"exports": [
|
|
10680
|
+
{
|
|
10681
|
+
"kind": "js",
|
|
10682
|
+
"name": "readColor",
|
|
10683
|
+
"declaration": {
|
|
10684
|
+
"name": "readColor",
|
|
10685
|
+
"module": "src/utils/read-color/read-color.ts"
|
|
10686
|
+
}
|
|
10687
|
+
}
|
|
10688
|
+
]
|
|
10689
|
+
},
|
|
10477
10690
|
{
|
|
10478
10691
|
"kind": "javascript-module",
|
|
10479
10692
|
"path": "src/utils/read-series/read-series.ts",
|
|
@@ -7,7 +7,9 @@ export type TokenColor = [number, number, number];
|
|
|
7
7
|
*
|
|
8
8
|
* **Two hops, and both are load-bearing.** A custom property's own value is the
|
|
9
9
|
* unevaluated function text, so `getComputedStyle` on a probe is the only way
|
|
10
|
-
* to see what it resolves to.
|
|
10
|
+
* to see what it resolves to. The second hop is `readColor`, which is shared
|
|
11
|
+
* with every other caller that has a resolved color and needs channels: what it
|
|
12
|
+
* resolves to is not `rgb()`, the
|
|
11
13
|
* palette derives from an `oklch()` seed, and Chrome answers `lab(100 0 0)` for
|
|
12
14
|
* white. Parsing that with a regular expression for numbers reads the three
|
|
13
15
|
* channels as 100, 0 and 0, which divided by 255 is a dark red.
|
|
@@ -33,8 +35,6 @@ export type TokenColor = [number, number, number];
|
|
|
33
35
|
export declare class TokenColorController implements ReactiveController {
|
|
34
36
|
private readonly host;
|
|
35
37
|
private readonly onChange?;
|
|
36
|
-
/** Where a resolved color is converted, one pixel at a time. */
|
|
37
|
-
private context;
|
|
38
38
|
/** Watches the document for `data-color-mode`. */
|
|
39
39
|
private modes;
|
|
40
40
|
/** The hidden element a token is resolved against. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { readColor } from '../../utils/read-color/read-color.js';
|
|
1
2
|
export class TokenColorController {
|
|
2
3
|
constructor(host, onChange) {
|
|
3
4
|
this.host = host;
|
|
4
5
|
this.onChange = onChange;
|
|
5
|
-
this.context = null;
|
|
6
6
|
this.announce = () => {
|
|
7
7
|
this.host.requestUpdate();
|
|
8
8
|
this.onChange?.();
|
|
@@ -16,10 +16,6 @@ export class TokenColorController {
|
|
|
16
16
|
this.probe = document.createElement('span');
|
|
17
17
|
this.probe.style.display = 'none';
|
|
18
18
|
document.body.appendChild(this.probe);
|
|
19
|
-
const swatch = document.createElement('canvas');
|
|
20
|
-
swatch.width = 1;
|
|
21
|
-
swatch.height = 1;
|
|
22
|
-
this.context = swatch.getContext('2d', { willReadFrequently: true });
|
|
23
19
|
this.modes = new MutationObserver(() => this.announce());
|
|
24
20
|
this.modes.observe(document.documentElement, {
|
|
25
21
|
attributeFilter: ['data-color-mode'],
|
|
@@ -35,18 +31,18 @@ export class TokenColorController {
|
|
|
35
31
|
this.scheme = undefined;
|
|
36
32
|
this.probe?.remove();
|
|
37
33
|
this.probe = undefined;
|
|
38
|
-
this.context = null;
|
|
39
34
|
}
|
|
40
35
|
read(token, fallback) {
|
|
41
|
-
const {
|
|
42
|
-
if (!
|
|
36
|
+
const { probe } = this;
|
|
37
|
+
if (!probe) {
|
|
43
38
|
return [...fallback];
|
|
44
39
|
}
|
|
45
40
|
probe.style.color = `var(${token})`;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
const read = readColor(getComputedStyle(probe).color);
|
|
42
|
+
if (!read) {
|
|
43
|
+
return [...fallback];
|
|
44
|
+
}
|
|
45
|
+
const [r, g, b] = read;
|
|
46
|
+
return [r / 255, g / 255, b / 255];
|
|
51
47
|
}
|
|
52
48
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { anchorElement } from './utils/anchor-element/anchor-element.js';
|
|
|
37
37
|
export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
|
|
38
38
|
export { closestInstance } from './utils/closest-instance/closest-instance.js';
|
|
39
39
|
export { createSheet } from './utils/create-sheet/create-sheet.js';
|
|
40
|
+
export { type Color, type ColorFormat, formatColor, parseColor, splitColors, toCss, } from './utils/css-color/css-color.js';
|
|
40
41
|
export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
|
|
41
42
|
export { type DrawIconOptions, drawIcon } from './utils/draw-icon/draw-icon.js';
|
|
42
43
|
export { CHART_PLOT, drawTooltip } from './utils/draw-tooltip/draw-tooltip.js';
|
|
@@ -50,6 +51,7 @@ export { type Curve, linePath, type Point } from './utils/line-path/line-path.js
|
|
|
50
51
|
export { markRegion, type RegionOptions } from './utils/mark-region/mark-region.js';
|
|
51
52
|
export { copiesOf, type MediaCopy, sameCopies, } from './utils/media-copy/media-copy.js';
|
|
52
53
|
export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
|
|
54
|
+
export { type ColorBytes, readColor } from './utils/read-color/read-color.js';
|
|
53
55
|
export { type Row, type RowCell, readRows, readSeries, type Series, type SeriesPoint, } from './utils/read-series/read-series.js';
|
|
54
56
|
export { type Relative, relativeTime, } from './utils/relative-time/relative-time.js';
|
|
55
57
|
export { type ChangeRequestOptions, requestChange, } from './utils/request-change/request-change.js';
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export { anchorElement } from './utils/anchor-element/anchor-element.js';
|
|
|
37
37
|
export { capturePointer } from './utils/capture-pointer/capture-pointer.js';
|
|
38
38
|
export { closestInstance } from './utils/closest-instance/closest-instance.js';
|
|
39
39
|
export { createSheet } from './utils/create-sheet/create-sheet.js';
|
|
40
|
+
export { formatColor, parseColor, splitColors, toCss, } from './utils/css-color/css-color.js';
|
|
40
41
|
export { customElement, definedTags, duplicateRegistrations, } from './utils/custom-element/custom-element.js';
|
|
41
42
|
export { drawIcon } from './utils/draw-icon/draw-icon.js';
|
|
42
43
|
export { CHART_PLOT, drawTooltip } from './utils/draw-tooltip/draw-tooltip.js';
|
|
@@ -50,6 +51,7 @@ export { linePath } from './utils/line-path/line-path.js';
|
|
|
50
51
|
export { markRegion } from './utils/mark-region/mark-region.js';
|
|
51
52
|
export { copiesOf, sameCopies, } from './utils/media-copy/media-copy.js';
|
|
52
53
|
export { onUnlessFalse } from './utils/on-unless-false/on-unless-false.js';
|
|
54
|
+
export { readColor } from './utils/read-color/read-color.js';
|
|
53
55
|
export { readRows, readSeries, } from './utils/read-series/read-series.js';
|
|
54
56
|
export { relativeTime, } from './utils/relative-time/relative-time.js';
|
|
55
57
|
export { requestChange, } from './utils/request-change/request-change.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading and writing the color syntaxes a field is allowed to hold.
|
|
3
|
+
*
|
|
4
|
+
* **The working model is HSB**, hue and saturation and brightness, because that
|
|
5
|
+
* is what the panel is: a square of saturation against brightness with a hue
|
|
6
|
+
* ramp beside it. HSL would put the square's corners somewhere a reader does not
|
|
7
|
+
* expect, since full saturation in HSL is a mid-lightness band rather than a
|
|
8
|
+
* corner.
|
|
9
|
+
*
|
|
10
|
+
* **Every syntax here is read; three are written.** A field that refuses
|
|
11
|
+
* `rgb(255 0 0)` because it prefers hex is a field arguing with the person using
|
|
12
|
+
* it, so the space-separated forms, the comma forms, `rgba` and `hsla` are all
|
|
13
|
+
* taken. What is written back is whichever of hex, rgb or hsl the component is
|
|
14
|
+
* set to, so a value stays in the shape the consumer asked for however it was
|
|
15
|
+
* entered.
|
|
16
|
+
*
|
|
17
|
+
* **What is written is the comma form.** Both are valid, and the commas are what
|
|
18
|
+
* a reader recognizes, what most code around them holds, and what pastes into
|
|
19
|
+
* anything without a second thought. A syntax a value is handed *out* in is not
|
|
20
|
+
* the place to be early. `rgba` and `hsla` come with them, since a comma form
|
|
21
|
+
* with a fourth argument is spelled that way.
|
|
22
|
+
*
|
|
23
|
+
* **Core's rather than a field's, because the CSS color grammar is the
|
|
24
|
+
* platform's.** Nothing here needs a color picker to make sense, and two
|
|
25
|
+
* families read it already. It is arithmetic with no DOM in it, so it runs on a
|
|
26
|
+
* server, which is also why named colors are refused below: resolving
|
|
27
|
+
* `rebeccapurple` means asking a browser that is not there.
|
|
28
|
+
*
|
|
29
|
+
* **One decision in here is a default rather than a capability.** What
|
|
30
|
+
* `formatColor` writes back, the comma form and an alpha only where one was
|
|
31
|
+
* asked for, is a choice about what a value looks like when it is handed out.
|
|
32
|
+
* It is core's choice now instead of the picker's, and a caller wanting another
|
|
33
|
+
* shape formats it itself.
|
|
34
|
+
*/
|
|
35
|
+
/** A color as the panel holds it. Alpha and the two percentages are 0 to 100. */
|
|
36
|
+
export interface Color {
|
|
37
|
+
alpha: number;
|
|
38
|
+
brightness: number;
|
|
39
|
+
hue: number;
|
|
40
|
+
saturation: number;
|
|
41
|
+
}
|
|
42
|
+
/** The syntaxes a value can be written in. */
|
|
43
|
+
export type ColorFormat = 'hex' | 'hsl' | 'rgb';
|
|
44
|
+
/**
|
|
45
|
+
* Splits a comma-separated list of colors, ignoring the commas inside them.
|
|
46
|
+
*
|
|
47
|
+
* A plain `split(',')` cannot hold the legacy syntaxes:
|
|
48
|
+
* `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,
|
|
49
|
+
* four of them not colors, and the row quietly renders three swatches instead of
|
|
50
|
+
* four. The modern space-separated forms survive it, which is what makes such a
|
|
51
|
+
* list look like it works.
|
|
52
|
+
*
|
|
53
|
+
* Depth rather than a regular expression, because the thing that matters is
|
|
54
|
+
* whether a comma is inside a function call and that is exactly what a counter
|
|
55
|
+
* answers. No color syntax nests, so a single counter is enough.
|
|
56
|
+
*/
|
|
57
|
+
export declare const splitColors: (value: string) => string[];
|
|
58
|
+
/**
|
|
59
|
+
* Any of the syntaxes to the working model, or `null` where it is not a color.
|
|
60
|
+
*
|
|
61
|
+
* Named colors are not here. Resolving `rebeccapurple` means either shipping
|
|
62
|
+
* the table of a hundred and forty or asking the browser, and the browser is not
|
|
63
|
+
* present on the server: a field that accepted a name client-side and rejected
|
|
64
|
+
* it during a server render would be worse than one that never took it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const parseColor: (value: string) => Color | null;
|
|
67
|
+
/**
|
|
68
|
+
* The working model back out, in the syntax asked for.
|
|
69
|
+
*
|
|
70
|
+
* The alpha is written only where the component carries one. A consumer who did
|
|
71
|
+
* not ask for transparency should not have to strip `ff` off every hex or read
|
|
72
|
+
* past a fourth argument they never wanted.
|
|
73
|
+
*/
|
|
74
|
+
export declare const formatColor: (color: Color, format: ColorFormat, withAlpha: boolean) => string;
|
|
75
|
+
/** The color as CSS, for the swatches and the tracks. Always with its alpha. */
|
|
76
|
+
export declare const toCss: (color: Color) => string;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
2
|
+
const toAlpha = (token) => {
|
|
3
|
+
if (token === undefined) {
|
|
4
|
+
return 100;
|
|
5
|
+
}
|
|
6
|
+
const number = Number.parseFloat(token);
|
|
7
|
+
if (Number.isNaN(number)) {
|
|
8
|
+
return 100;
|
|
9
|
+
}
|
|
10
|
+
return clamp(token.includes('%') ? number : number * 100, 0, 100);
|
|
11
|
+
};
|
|
12
|
+
const toByte = (token) => {
|
|
13
|
+
const number = Number.parseFloat(token);
|
|
14
|
+
return clamp(token.includes('%') ? (number / 100) * 255 : number, 0, 255);
|
|
15
|
+
};
|
|
16
|
+
const rgbToHsb = (red, green, blue, alpha) => {
|
|
17
|
+
const r = red / 255;
|
|
18
|
+
const g = green / 255;
|
|
19
|
+
const b = blue / 255;
|
|
20
|
+
const high = Math.max(r, g, b);
|
|
21
|
+
const low = Math.min(r, g, b);
|
|
22
|
+
const span = high - low;
|
|
23
|
+
let hue = 0;
|
|
24
|
+
if (span !== 0) {
|
|
25
|
+
if (high === r) {
|
|
26
|
+
hue = (g - b) / span + (g < b ? 6 : 0);
|
|
27
|
+
}
|
|
28
|
+
else if (high === g) {
|
|
29
|
+
hue = (b - r) / span + 2;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
hue = (r - g) / span + 4;
|
|
33
|
+
}
|
|
34
|
+
hue *= 60;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
alpha,
|
|
38
|
+
brightness: Math.round(high * 100),
|
|
39
|
+
hue: Math.round(hue),
|
|
40
|
+
saturation: Math.round(high === 0 ? 0 : (span / high) * 100),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const toRgb = (color) => {
|
|
44
|
+
const hue = (((color.hue % 360) + 360) % 360) / 60;
|
|
45
|
+
const saturation = color.saturation / 100;
|
|
46
|
+
const brightness = color.brightness / 100;
|
|
47
|
+
const chroma = brightness * saturation;
|
|
48
|
+
const middle = chroma * (1 - Math.abs((hue % 2) - 1));
|
|
49
|
+
const base = brightness - chroma;
|
|
50
|
+
const sextants = [
|
|
51
|
+
[chroma, middle, 0],
|
|
52
|
+
[middle, chroma, 0],
|
|
53
|
+
[0, chroma, middle],
|
|
54
|
+
[0, middle, chroma],
|
|
55
|
+
[middle, 0, chroma],
|
|
56
|
+
[chroma, 0, middle],
|
|
57
|
+
];
|
|
58
|
+
const [red, green, blue] = sextants[Math.floor(hue) % 6] ?? [0, 0, 0];
|
|
59
|
+
return {
|
|
60
|
+
blue: Math.round((blue + base) * 255),
|
|
61
|
+
green: Math.round((green + base) * 255),
|
|
62
|
+
red: Math.round((red + base) * 255),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const toHsl = (color) => {
|
|
66
|
+
const saturation = color.saturation / 100;
|
|
67
|
+
const brightness = color.brightness / 100;
|
|
68
|
+
const lightness = (brightness * (2 - saturation)) / 2;
|
|
69
|
+
return {
|
|
70
|
+
hue: ((color.hue % 360) + 360) % 360,
|
|
71
|
+
lightness: Math.round(lightness * 100),
|
|
72
|
+
saturation: Math.round((lightness === 0 || lightness === 1
|
|
73
|
+
? 0
|
|
74
|
+
: (brightness - lightness) / Math.min(lightness, 1 - lightness)) * 100),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export const splitColors = (value) => {
|
|
78
|
+
const found = [];
|
|
79
|
+
let depth = 0;
|
|
80
|
+
let current = '';
|
|
81
|
+
for (const character of value) {
|
|
82
|
+
if (character === '(') {
|
|
83
|
+
depth += 1;
|
|
84
|
+
}
|
|
85
|
+
else if (character === ')') {
|
|
86
|
+
depth = Math.max(0, depth - 1);
|
|
87
|
+
}
|
|
88
|
+
if (character === ',' && depth === 0) {
|
|
89
|
+
found.push(current.trim());
|
|
90
|
+
current = '';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
current += character;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
found.push(current.trim());
|
|
97
|
+
return found.filter(Boolean);
|
|
98
|
+
};
|
|
99
|
+
export const parseColor = (value) => {
|
|
100
|
+
const text = value.trim().toLowerCase();
|
|
101
|
+
if (text === '') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (text.startsWith('#')) {
|
|
105
|
+
const digits = text.slice(1);
|
|
106
|
+
const short = digits.length === 3 || digits.length === 4;
|
|
107
|
+
const long = digits.length === 6 || digits.length === 8;
|
|
108
|
+
if ((!short && !long) || !/^[\da-f]+$/.test(digits)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const width = short ? 1 : 2;
|
|
112
|
+
const channel = (index) => {
|
|
113
|
+
const piece = digits.slice(index * width, index * width + width);
|
|
114
|
+
return Number.parseInt(short ? piece + piece : piece, 16);
|
|
115
|
+
};
|
|
116
|
+
const carriesAlpha = digits.length === 4 || digits.length === 8;
|
|
117
|
+
return rgbToHsb(channel(0), channel(1), channel(2), carriesAlpha ? Math.round((channel(3) / 255) * 100) : 100);
|
|
118
|
+
}
|
|
119
|
+
const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
|
|
120
|
+
if (!call?.[1] || call[2] === undefined) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const [first = '', second = '', third = '', fourth] = call[2]
|
|
124
|
+
.replace(/[,/]/g, ' ')
|
|
125
|
+
.split(/\s+/)
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
if (first === '' || second === '' || third === '') {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const alpha = toAlpha(fourth);
|
|
131
|
+
if (call[1].startsWith('rgb')) {
|
|
132
|
+
return rgbToHsb(toByte(first), toByte(second), toByte(third), alpha);
|
|
133
|
+
}
|
|
134
|
+
const hue = Number.parseFloat(first);
|
|
135
|
+
const saturation = clamp(Number.parseFloat(second), 0, 100) / 100;
|
|
136
|
+
const lightness = clamp(Number.parseFloat(third), 0, 100) / 100;
|
|
137
|
+
if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
|
|
141
|
+
return {
|
|
142
|
+
alpha,
|
|
143
|
+
brightness: Math.round(brightness * 100),
|
|
144
|
+
hue: Math.round(((hue % 360) + 360) % 360),
|
|
145
|
+
saturation: Math.round((brightness === 0 ? 0 : 2 * (1 - lightness / brightness)) * 100),
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
export const formatColor = (color, format, withAlpha) => {
|
|
149
|
+
const alpha = clamp(color.alpha, 0, 100) / 100;
|
|
150
|
+
const opacity = Number(alpha.toFixed(2));
|
|
151
|
+
if (format === 'hsl') {
|
|
152
|
+
const { hue, lightness, saturation } = toHsl(color);
|
|
153
|
+
const body = `${Math.round(hue)}, ${saturation}%, ${lightness}%`;
|
|
154
|
+
return withAlpha ? `hsla(${body}, ${opacity})` : `hsl(${body})`;
|
|
155
|
+
}
|
|
156
|
+
const { blue, green, red } = toRgb(color);
|
|
157
|
+
if (format === 'rgb') {
|
|
158
|
+
const body = `${red}, ${green}, ${blue}`;
|
|
159
|
+
return withAlpha ? `rgba(${body}, ${opacity})` : `rgb(${body})`;
|
|
160
|
+
}
|
|
161
|
+
const byte = (one) => one.toString(16).padStart(2, '0');
|
|
162
|
+
const hex = `#${byte(red)}${byte(green)}${byte(blue)}`;
|
|
163
|
+
return withAlpha ? hex + byte(Math.round(alpha * 255)) : hex;
|
|
164
|
+
};
|
|
165
|
+
export const toCss = (color) => {
|
|
166
|
+
const { blue, green, red } = toRgb(color);
|
|
167
|
+
return `rgb(${red} ${green} ${blue} / ${clamp(color.alpha, 0, 100)}%)`;
|
|
168
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a computed color the engine has resolved but not converted.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `css-color`, which reads the syntax a *consumer* writes and
|
|
5
|
+
* runs on a server. This reads what the engine hands back, which can be in any
|
|
6
|
+
* space the platform supports, and needs a canvas to do it.
|
|
7
|
+
*/
|
|
8
|
+
/** A color as three sRGB bytes, which is what a canvas reports and takes. */
|
|
9
|
+
export type ColorBytes = [number, number, number];
|
|
10
|
+
/**
|
|
11
|
+
* A computed color as three sRGB bytes, or nothing where it cannot be read.
|
|
12
|
+
*
|
|
13
|
+
* **Painting it is the only way to read it.** A color-typed property is
|
|
14
|
+
* resolved before it is reported, but resolved is not the same as converted:
|
|
15
|
+
* Chrome hands back `lab(100 0 0)` for a token built through `oklab`, and
|
|
16
|
+
* scraping the first three numbers out of that and calling them red, green and
|
|
17
|
+
* blue made white arrive as `rgb(100, 0, 0)`. In light mode the near dots were
|
|
18
|
+
* `lab(0 0 0)`, which scrapes to black and looks right, so only dark mode
|
|
19
|
+
* showed it: dark red mixed toward black, on a near-black ground.
|
|
20
|
+
*
|
|
21
|
+
* **Painted twice, over two different grounds, which is what makes the refusal
|
|
22
|
+
* honest.** Assigning a value the engine will not parse leaves `fillStyle`
|
|
23
|
+
* exactly where it was, so a single paint cannot tell a color apart from the
|
|
24
|
+
* ground it failed to replace. Over black and over white, a value that parsed
|
|
25
|
+
* gives the same answer twice and one that did not gives two. A caller then
|
|
26
|
+
* chooses its own fallback rather than being handed a color nobody picked,
|
|
27
|
+
* which is the half both earlier copies of this had to guess at.
|
|
28
|
+
*
|
|
29
|
+
* A readback is not free, so the answer is held against the string it came
|
|
30
|
+
* from, refusals included. The oldest goes when the store is full rather than
|
|
31
|
+
* all of them, or a page deriving colors on a slider pays for every frame
|
|
32
|
+
* twice.
|
|
33
|
+
*
|
|
34
|
+
* @param value A resolved color, as `getComputedStyle` reports one.
|
|
35
|
+
*/
|
|
36
|
+
export declare const readColor: (value: string) => ColorBytes | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
let swatch;
|
|
2
|
+
const known = new Map();
|
|
3
|
+
const KEPT = 64;
|
|
4
|
+
const painted = (context, value, under) => {
|
|
5
|
+
context.fillStyle = under;
|
|
6
|
+
context.fillStyle = value;
|
|
7
|
+
context.fillRect(0, 0, 1, 1);
|
|
8
|
+
const [red, green, blue] = context.getImageData(0, 0, 1, 1).data;
|
|
9
|
+
return [red ?? 0, green ?? 0, blue ?? 0];
|
|
10
|
+
};
|
|
11
|
+
export const readColor = (value) => {
|
|
12
|
+
if (known.has(value)) {
|
|
13
|
+
const seen = known.get(value);
|
|
14
|
+
return seen ? [...seen] : undefined;
|
|
15
|
+
}
|
|
16
|
+
if (typeof document === 'undefined') {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (swatch === undefined) {
|
|
20
|
+
swatch = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
21
|
+
}
|
|
22
|
+
if (!swatch) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const over = painted(swatch, value, '#000000');
|
|
26
|
+
const under = painted(swatch, value, '#ffffff');
|
|
27
|
+
const read = over.every((one, at) => one === under[at]) ? over : undefined;
|
|
28
|
+
if (known.size >= KEPT) {
|
|
29
|
+
const oldest = known.keys().next().value;
|
|
30
|
+
if (oldest !== undefined) {
|
|
31
|
+
known.delete(oldest);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
known.set(value, read);
|
|
35
|
+
return read ? [...read] : undefined;
|
|
36
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusion.dev/helix-core",
|
|
3
3
|
"description": "Helix element primitives: base classes, mixins, decorators, utilities.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.58.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ross Sheppard <ross.sheppard@fusion.dev>",
|
|
7
7
|
"repository": {
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"bugs": "https://github.com/project-fusion/fusion/issues",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@floating-ui/dom": "^1.8.0",
|
|
37
|
-
"@fusion.dev/helix-icons": "0.
|
|
37
|
+
"@fusion.dev/helix-icons": "0.58.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
41
|
-
"@fusion.dev/helix-cem": "0.
|
|
42
|
-
"@fusion.dev/helix-testing": "0.
|
|
41
|
+
"@fusion.dev/helix-cem": "0.58.0",
|
|
42
|
+
"@fusion.dev/helix-testing": "0.58.0",
|
|
43
43
|
"lit": "^3.2.1"
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://fusion.dev",
|