@fusion.dev/helix-core 0.149.1 → 0.151.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 (32) hide show
  1. package/custom-elements.json +482 -8
  2. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +62 -5
  3. package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +112 -12
  4. package/dist/events/details.d.ts +14 -0
  5. package/dist/events/events.d.ts +6 -2
  6. package/dist/events/events.js +5 -1
  7. package/dist/events/names.js +2 -1
  8. package/dist/events/reading-change.d.ts +14 -0
  9. package/dist/events/reading-change.js +11 -0
  10. package/dist/events/reading-press.d.ts +13 -0
  11. package/dist/events/reading-press.js +11 -0
  12. package/dist/helix-element/helix-element.js +2 -1
  13. package/dist/index.d.ts +4 -3
  14. package/dist/index.js +3 -2
  15. package/dist/mixins/mixin-collection/mixin-collection.d.ts +5 -0
  16. package/dist/mixins/mixin-collection/mixin-collection.js +7 -1
  17. package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +34 -0
  18. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +3 -2
  19. package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +1 -0
  20. package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +8 -0
  21. package/dist/mixins/mixin-tokens/mixin-tokens.js +73 -0
  22. package/dist/utils/chord/key-names.d.ts +14 -2
  23. package/dist/utils/chord/key-names.js +58 -3
  24. package/dist/utils/date-segments/date-segments.d.ts +5 -4
  25. package/dist/utils/date-segments/date-segments.js +12 -0
  26. package/dist/utils/draw-value-axis/draw-value-axis.d.ts +22 -0
  27. package/dist/utils/draw-value-axis/draw-value-axis.js +9 -0
  28. package/dist/utils/read-color/read-color.d.ts +24 -8
  29. package/dist/utils/read-color/read-color.js +20 -8
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/package.json +4 -4
@@ -5,6 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { property, state } from 'lit/decorators.js';
8
+ const caretKeys = new Set(['ArrowLeft', 'ArrowRight', 'Backspace', 'Delete', 'End', 'Home']);
8
9
  export const MixinPickerEditing = (Base) => {
9
10
  class PickerEditingElement extends Base {
10
11
  constructor() {
@@ -18,6 +19,37 @@ export const MixinPickerEditing = (Base) => {
18
19
  this.handlePress = (event) => {
19
20
  this.pressing = event.type === 'pointerdown' && event.composedPath()[0] === this.control;
20
21
  };
22
+ this.handleEditKey = (event) => {
23
+ const control = this.control;
24
+ if (this.editing ||
25
+ this.readOnly ||
26
+ this.isDisabled ||
27
+ !control ||
28
+ event.composedPath()[0] !== control ||
29
+ event.altKey ||
30
+ event.ctrlKey ||
31
+ event.metaKey ||
32
+ event.isComposing ||
33
+ (event.key.length !== 1 && !caretKeys.has(event.key))) {
34
+ return;
35
+ }
36
+ const shown = control.value.length;
37
+ const start = control.selectionStart ?? shown;
38
+ const end = control.selectionEnd ?? shown;
39
+ this.editing = true;
40
+ const text = this.display;
41
+ if (text === control.value) {
42
+ return;
43
+ }
44
+ control.value = text;
45
+ if (start === 0 && end === shown) {
46
+ control.setSelectionRange(0, text.length);
47
+ }
48
+ else {
49
+ const at = end === 0 ? 0 : text.length;
50
+ control.setSelectionRange(at, at);
51
+ }
52
+ };
21
53
  this.handleFocus = (event) => {
22
54
  this.editing = !this.returning;
23
55
  const pressed = this.pressing;
@@ -49,12 +81,14 @@ export const MixinPickerEditing = (Base) => {
49
81
  this.addEventListener('pointerdown', this.handlePress);
50
82
  this.addEventListener('pointercancel', this.handlePress);
51
83
  this.addEventListener('click', this.handlePress);
84
+ this.addEventListener('keydown', this.handleEditKey);
52
85
  }
53
86
  disconnectedCallback() {
54
87
  super.disconnectedCallback();
55
88
  this.removeEventListener('pointerdown', this.handlePress);
56
89
  this.removeEventListener('pointercancel', this.handlePress);
57
90
  this.removeEventListener('click', this.handlePress);
91
+ this.removeEventListener('keydown', this.handleEditKey);
58
92
  }
59
93
  }
60
94
  __decorate([
@@ -62,8 +62,9 @@ export declare class MixinSegmentedFieldInterface {
62
62
  * `04.08.2026` in German and `08/04/2026` in English with nothing to parse.
63
63
  *
64
64
  * **What a segment does with a key**, which is React Aria's: digits fill it and
65
- * move on once it cannot take another, Up and Down step it and wrap, Home and
66
- * End go to its ends, Backspace and Delete empty it, Left and Right move between
65
+ * move on once it cannot take another, Up and Down step it and wrap, Page Up
66
+ * and Page Down step it by a page, a week of days, five years or fifteen
67
+ * minutes, Home and End go to its ends, Backspace and Delete empty it, Left and Right move between
67
68
  * segments along the line of text, and Alt+Down opens the picker. Each segment
68
69
  * is a tab stop, as each is in React Aria.
69
70
  *
@@ -236,6 +236,7 @@ export const MixinSegmentedField = (Base) => {
236
236
  aria-valuenow=${spin ? (shown ?? nothing) : nothing}
237
237
  aria-valuetext=${spin ? (empty ? this.emptySegmentLabel : segmentValueText(kind, this.parts, twelve, locale)) : nothing}
238
238
  aria-disabled=${this.isDisabled ? 'true' : nothing}
239
+ aria-required=${this.required ? 'true' : nothing}
239
240
  aria-readonly=${this.readOnly ? 'true' : nothing}
240
241
  @keydown=${this.handleSegmentKeydown}
241
242
  >${text}</span
@@ -99,6 +99,14 @@ export declare class MixinTokensInterface {
99
99
  * of its selection, and a filter field takes a token out of its own list.
100
100
  */
101
101
  protected removeLastToken(): void;
102
+ /**
103
+ * The tokens the arrow keys move between, in the order they are shown.
104
+ *
105
+ * Each is an element whose remove button is its `dismiss` part and whose
106
+ * `dismiss()` asks it to go, which `Tag` and `FilterInput.Chip` both are.
107
+ * None by default.
108
+ */
109
+ protected tokenElements(): HTMLElement[];
102
110
  }
103
111
  /**
104
112
  * A row of tokens inside a field, and the behavior that goes with it.
@@ -8,6 +8,7 @@ import { caretUp } from '@fusion.dev/helix-icons';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { drawIcon } from '../../utils/draw-icon/draw-icon.js';
11
+ import { inlineStep } from '../../utils/inline-step/inline-step.js';
11
12
  import { formatWords, words } from '../../words/words.js';
12
13
  export const MixinTokens = (Base) => {
13
14
  class TokensElement extends Base {
@@ -22,6 +23,42 @@ export const MixinTokens = (Base) => {
22
23
  this.toggleExpanded = () => {
23
24
  this.expanded = !this.expanded;
24
25
  };
26
+ this.handleTokenKeydown = (event) => {
27
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey) {
28
+ return;
29
+ }
30
+ const stops = this.tokenStops();
31
+ const path = event.composedPath();
32
+ const at = stops.findIndex((stop) => path.includes(stop));
33
+ if (at < 0) {
34
+ return;
35
+ }
36
+ const step = inlineStep(this, event.key);
37
+ if (step !== 0) {
38
+ event.preventDefault();
39
+ const next = stops[at + step];
40
+ if (next) {
41
+ next.focus();
42
+ }
43
+ else if (step > 0) {
44
+ this.tokenControl?.focus();
45
+ this.tokenControl?.setSelectionRange(0, 0);
46
+ }
47
+ return;
48
+ }
49
+ if (event.key !== 'Delete' && event.key !== 'Backspace') {
50
+ return;
51
+ }
52
+ event.preventDefault();
53
+ const stop = stops[at];
54
+ const token = this.tokenElements().find((one) => one.shadowRoot?.contains(stop ?? null));
55
+ token?.dismiss?.();
56
+ requestAnimationFrame(() => {
57
+ if (!this.matches(':focus-within')) {
58
+ this.tokenControl?.focus();
59
+ }
60
+ });
61
+ };
25
62
  }
26
63
  static { this.words = ''; }
27
64
  get shownLimit() {
@@ -30,6 +67,26 @@ export const MixinTokens = (Base) => {
30
67
  renderTokenRow(content, over) {
31
68
  return html `<span part="tags">${content}${this.renderRest(over)}</span>`;
32
69
  }
70
+ connectedCallback() {
71
+ super.connectedCallback();
72
+ this.addEventListener('keydown', this.handleTokenKeydown);
73
+ }
74
+ disconnectedCallback() {
75
+ super.disconnectedCallback();
76
+ this.removeEventListener('keydown', this.handleTokenKeydown);
77
+ }
78
+ tokenElements() {
79
+ return [];
80
+ }
81
+ tokenStops() {
82
+ return this.tokenElements()
83
+ .filter((token) => !token.hidden)
84
+ .map((token) => token.shadowRoot?.querySelector('[part~="dismiss"]'))
85
+ .filter((stop) => stop !== null && stop !== undefined);
86
+ }
87
+ get tokenControl() {
88
+ return this.renderRoot.querySelector('[part~="control"]');
89
+ }
33
90
  renderRest(over) {
34
91
  if (!this.expandable) {
35
92
  return over > 0
@@ -59,6 +116,22 @@ export const MixinTokens = (Base) => {
59
116
  return limit > 0 ? Math.max(0, tokens.length - limit) : 0;
60
117
  }
61
118
  tokenKeydown(event, typed) {
119
+ const control = event.target;
120
+ if (inlineStep(this, event.key) === -1 &&
121
+ !event.shiftKey &&
122
+ !event.altKey &&
123
+ !event.ctrlKey &&
124
+ !event.metaKey &&
125
+ control?.selectionStart === 0 &&
126
+ control.selectionEnd === 0) {
127
+ const last = this.tokenStops().at(-1);
128
+ if (last) {
129
+ event.preventDefault();
130
+ last.focus();
131
+ return true;
132
+ }
133
+ return false;
134
+ }
62
135
  if (event.key !== 'Backspace' || typed !== '') {
63
136
  return false;
64
137
  }
@@ -7,10 +7,16 @@
7
7
  */
8
8
  /** A chord's keys as they are shown, and what is shown between two of them. */
9
9
  export interface KeyNames {
10
- /** Each key's label, in the order the chord names them. */
10
+ /** Each key's label, modifiers first in the platform's order, then the rest as written. */
11
11
  keys: string[];
12
12
  /** What goes between two keys: `+`, a hyphen, or nothing. */
13
13
  separator: string;
14
+ /**
15
+ * The chord in words for a screen reader, such as `Shift Command P`, where a
16
+ * label is a glyph a reader would hear as a symbol. Empty when every label
17
+ * is a word already.
18
+ */
19
+ spoken: string;
14
20
  }
15
21
  /**
16
22
  * The labels of a chord's keys on a platform, with `own` labels in place of
@@ -18,7 +24,9 @@ export interface KeyNames {
18
24
  *
19
25
  * `parts` are the chord's names as `chordParts` splits them. A name `own`
20
26
  * holds takes that label, a name the platform knows takes its glyph or word,
21
- * and any other single character is shown in capitals.
27
+ * and any other single character is shown in capitals. **Modifiers come first,
28
+ * in the platform's order**, so one `keys` value is right on every platform:
29
+ * `mod+shift+p` is `⇧⌘P` on an Apple keyboard and `Ctrl+Shift+P` elsewhere.
22
30
  *
23
31
  * **On an Apple keyboard glyphs run together and words are joined with a
24
32
  * hyphen**, as Apple writes `Command-K`. A label of more than one character
@@ -26,5 +34,9 @@ export interface KeyNames {
26
34
  * nothing between the two ran them into `CommandK`. The platform's own `Esc`
27
35
  * and `Space` keep running on from a glyph, as `⌘Esc` always has. Everywhere
28
36
  * else keys are joined with `+`.
37
+ *
38
+ * `spoken` says the chord in words where a label is one of the platform's
39
+ * glyphs, such as `⌘` or `↑`, which a screen reader reads as a symbol or
40
+ * skips. A label from `own` is taken as words already.
29
41
  */
30
42
  export declare const keyNames: (parts: readonly string[], apple: boolean, own?: Readonly<Record<string, string>>) => KeyNames;
@@ -36,17 +36,72 @@ const ELSEWHERE = {
36
36
  tab: 'Tab',
37
37
  up: '↑',
38
38
  };
39
+ const SPOKEN = {
40
+ alt: 'Option',
41
+ backspace: 'Delete',
42
+ cmd: 'Command',
43
+ ctrl: 'Control',
44
+ del: 'Forward Delete',
45
+ down: 'Down Arrow',
46
+ enter: 'Return',
47
+ left: 'Left Arrow',
48
+ meta: 'Command',
49
+ mod: 'Command',
50
+ opt: 'Option',
51
+ right: 'Right Arrow',
52
+ shift: 'Shift',
53
+ tab: 'Tab',
54
+ up: 'Up Arrow',
55
+ };
56
+ const ORDER_APPLE = {
57
+ alt: 1,
58
+ cmd: 3,
59
+ ctrl: 0,
60
+ meta: 3,
61
+ mod: 3,
62
+ opt: 1,
63
+ shift: 2,
64
+ };
65
+ const ORDER_ELSEWHERE = {
66
+ alt: 2,
67
+ cmd: 0,
68
+ ctrl: 1,
69
+ meta: 0,
70
+ mod: 1,
71
+ opt: 2,
72
+ shift: 3,
73
+ };
74
+ const ordered = (parts, apple) => {
75
+ const order = apple ? ORDER_APPLE : ORDER_ELSEWHERE;
76
+ const rank = (part) => order[part.toLowerCase()] ?? Number.POSITIVE_INFINITY;
77
+ return parts
78
+ .map((part, index) => ({ index, part }))
79
+ .sort((a, b) => rank(a.part) - rank(b.part) || a.index - b.index)
80
+ .map(({ part }) => part);
81
+ };
39
82
  export const keyNames = (parts, apple, own = {}) => {
40
83
  const names = apple ? APPLE : ELSEWHERE;
41
84
  let worded = false;
42
- const keys = parts.map((part) => {
85
+ let glyphs = false;
86
+ const words = [];
87
+ const keys = ordered(parts, apple).map((part) => {
43
88
  const name = part.toLowerCase();
44
89
  const mine = own[name];
45
90
  if (mine !== undefined) {
46
91
  worded ||= [...mine].length > 1;
92
+ words.push(mine);
47
93
  return mine;
48
94
  }
49
- return names[name] ?? (part.length === 1 ? part.toUpperCase() : part);
95
+ const shown = names[name] ?? (part.length === 1 ? part.toUpperCase() : part);
96
+ const said = SPOKEN[name];
97
+ const glyph = said !== undefined && [...shown].length === 1;
98
+ glyphs ||= glyph;
99
+ words.push(glyph ? said : shown);
100
+ return shown;
50
101
  });
51
- return { keys, separator: apple ? (worded ? '-' : '') : '+' };
102
+ return {
103
+ keys,
104
+ separator: apple ? (worded ? '-' : '') : '+',
105
+ spoken: glyphs ? words.join(' ') : '',
106
+ };
52
107
  };
@@ -86,10 +86,11 @@ export interface SegmentKeyState {
86
86
  * What an editing key does to a segment, or `null` for a key that edits
87
87
  * nothing.
88
88
  *
89
- * The arrows step it and wrap, Home and End take it to its lowest and highest,
90
- * Backspace and Delete empty it, a digit is typed as `typeDigit` types it, and
91
- * on the meridiem a letter picks the half whose word starts with it. `advance`
92
- * says the segment is full and focus moves on.
89
+ * The arrows step it and wrap, Page Up and Page Down step it by a page, Home
90
+ * and End take it to its lowest and highest, Backspace and Delete empty it, a
91
+ * digit is typed as `typeDigit` types it, and on the meridiem a letter picks
92
+ * the half whose word starts with it. `advance` says the segment is full and
93
+ * focus moves on.
93
94
  */
94
95
  export declare const segmentKey: (key: string, { kind, locale, segments, twelve, typed }: SegmentKeyState) => {
95
96
  advance: boolean;
@@ -119,11 +119,23 @@ export const typeDigit = (segments, kind, typed, digit, twelve) => {
119
119
  const next = value >= min ? setSegment(segments, kind, value, twelve) : segments;
120
120
  return { advance: full && value >= min, segments: next, typed: full ? '' : text };
121
121
  };
122
+ const pageStep = {
123
+ day: 7,
124
+ hour: 2,
125
+ minute: 15,
126
+ month: 2,
127
+ second: 15,
128
+ year: 5,
129
+ };
122
130
  export const segmentKey = (key, { kind, locale, segments, twelve, typed }) => {
123
131
  const done = (next) => next === undefined ? null : { advance: false, segments: next, typed: '' };
124
132
  if (key === 'ArrowUp' || key === 'ArrowDown') {
125
133
  return done(stepSegment(segments, kind, key === 'ArrowUp' ? 1 : -1, twelve));
126
134
  }
135
+ if (key === 'PageUp' || key === 'PageDown') {
136
+ const by = (key === 'PageUp' ? 1 : -1) * (pageStep[kind] ?? 1);
137
+ return done(stepSegment(segments, kind, by, twelve));
138
+ }
127
139
  if (key === 'Home' || key === 'End') {
128
140
  const { max, min } = segmentRange(kind, segments, twelve);
129
141
  return done(setSegment(segments, kind, key === 'Home' ? min : max, twelve));
@@ -0,0 +1,22 @@
1
+ import { type TemplateResult } from 'lit';
2
+ /**
3
+ * The numbers of a chart's value scale, beside its drawing: an `axis` part
4
+ * holding a `tick` part at each level `ticks` picks, which are the levels
5
+ * `gridlines` rules.
6
+ *
7
+ * HTML beside the plot, for the reason a chart's reading names are: text
8
+ * inside an `<svg>` that fills its box scales with the box. Each tick is placed
9
+ * with `inset-block-start`, as a percentage of the axis, so a stylesheet that
10
+ * makes the axis as tall as the plot puts each number on its level.
11
+ *
12
+ * `width` builds the axis's style from the number of characters in its widest
13
+ * number. The ticks are positioned absolutely and give the column no width of
14
+ * their own, so each family writes its own custom property there, such as
15
+ * `--line-chart-axis` in `ch`, and a stylesheet sizes the column from it.
16
+ *
17
+ * @param domain The scale's lowest and highest values.
18
+ * @param place Where a value sits down the plot, from 0 at the top to 100 at the bottom.
19
+ * @param format How a value is written, such as a number in the chart's locale.
20
+ * @param width The style the axis carries, from the number of characters in its widest number.
21
+ */
22
+ export declare const drawValueAxis: (domain: readonly [number, number], place: (value: number) => number, format: (value: number) => string, width: (widest: number) => string) => TemplateResult;
@@ -0,0 +1,9 @@
1
+ import { html } from 'lit';
2
+ import { ticks } from '../scale/ticks.js';
3
+ export const drawValueAxis = (domain, place, format, width) => {
4
+ const levels = ticks(domain);
5
+ const widest = Math.max(1, ...levels.map((level) => format(level).length));
6
+ return html `<div aria-hidden="true" part="axis" style=${width(widest)}>
7
+ ${levels.map((level) => html `<span part="tick" style=${`inset-block-start: ${place(level)}%`}>${format(level)}</span>`)}
8
+ </div>`;
9
+ };
@@ -7,8 +7,16 @@
7
7
  */
8
8
  /** A color as three sRGB bytes, which is what a canvas reports and takes. */
9
9
  export type ColorBytes = [number, number, number];
10
+ /** A color as three sRGB bytes and its alpha, from 0 to 1. */
11
+ export type ColorBytesAlpha = [number, number, number, number];
10
12
  /**
11
- * A computed color as three sRGB bytes, or nothing where it cannot be read.
13
+ * `value` painted onto a clear pixel and read back, alpha and all, or nothing
14
+ * where the engine does not parse it.
15
+ */
16
+ export declare const paintedColor: (context: CanvasRenderingContext2D, value: string) => ColorBytesAlpha | undefined;
17
+ /**
18
+ * A computed color as three sRGB bytes and its alpha, or nothing where it
19
+ * cannot be read.
12
20
  *
13
21
  * **Painting it is the only way to read it.** A color-typed property is
14
22
  * resolved before it is reported, but resolved is not the same as converted:
@@ -18,13 +26,11 @@ export type ColorBytes = [number, number, number];
18
26
  * `lab(0 0 0)`, which scrapes to black and looks right, so only dark mode
19
27
  * showed it: dark red mixed toward black, on a near-black ground.
20
28
  *
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.
29
+ * **A value the engine does not parse is refused**, so a caller chooses its
30
+ * own fallback rather than being handed a color nobody picked. A see-through
31
+ * color is read with its alpha: `rgb(255 0 0 / 50%)` is `[255, 0, 0, 0.5]`.
32
+ * It was refused too, when the test for parsing was painting over black and
33
+ * over white, and Orb drew it gray.
28
34
  *
29
35
  * A readback is not free, so the answer is held against the string it came
30
36
  * from, refusals included. The oldest goes when the store is full rather than
@@ -33,6 +39,16 @@ export type ColorBytes = [number, number, number];
33
39
  *
34
40
  * @param value A resolved color, as `getComputedStyle` reports one.
35
41
  */
42
+ export declare const readColorAlpha: (value: string) => ColorBytesAlpha | undefined;
43
+ /**
44
+ * A computed color as three sRGB bytes, or nothing where it cannot be read or
45
+ * is see-through.
46
+ *
47
+ * `readColorAlpha` is the reading, and this keeps an opaque color only, which
48
+ * is what a caller with no use for alpha can draw as it is.
49
+ *
50
+ * @param value A resolved color, as `getComputedStyle` reports one.
51
+ */
36
52
  export declare const readColor: (value: string) => ColorBytes | undefined;
37
53
  /**
38
54
  * A computed color as `rgb()`, or `rgba()` where it is see-through, which
@@ -1,14 +1,24 @@
1
1
  let swatch;
2
2
  const known = new Map();
3
3
  const KEPT = 64;
4
- const painted = (context, value, under) => {
5
- context.fillStyle = under;
4
+ const parses = (context, value) => {
5
+ context.fillStyle = '#000000';
6
6
  context.fillStyle = value;
7
+ const overBlack = String(context.fillStyle);
8
+ context.fillStyle = '#ffffff';
9
+ context.fillStyle = value;
10
+ return String(context.fillStyle) === overBlack;
11
+ };
12
+ export const paintedColor = (context, value) => {
13
+ if (!parses(context, value)) {
14
+ return undefined;
15
+ }
16
+ context.clearRect(0, 0, 1, 1);
7
17
  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];
18
+ const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data;
19
+ return [red ?? 0, green ?? 0, blue ?? 0, Math.round(((alpha ?? 0) / 255) * 1000) / 1000];
10
20
  };
11
- export const readColor = (value) => {
21
+ export const readColorAlpha = (value) => {
12
22
  if (known.has(value)) {
13
23
  const seen = known.get(value);
14
24
  return seen ? [...seen] : undefined;
@@ -22,9 +32,7 @@ export const readColor = (value) => {
22
32
  if (!swatch) {
23
33
  return undefined;
24
34
  }
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;
35
+ const read = paintedColor(swatch, value);
28
36
  if (known.size >= KEPT) {
29
37
  const oldest = known.keys().next().value;
30
38
  if (oldest !== undefined) {
@@ -34,6 +42,10 @@ export const readColor = (value) => {
34
42
  known.set(value, read);
35
43
  return read ? [...read] : undefined;
36
44
  };
45
+ export const readColor = (value) => {
46
+ const read = readColorAlpha(value);
47
+ return read && read[3] === 1 ? [read[0], read[1], read[2]] : undefined;
48
+ };
37
49
  export const rgbOf = (color) => {
38
50
  const pixel = typeof document === 'undefined' ? null : document.createElement('canvas');
39
51
  const context = pixel?.getContext?.('2d', { willReadFrequently: true });
package/dist/version.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * release, so the two never disagree; a spec beside this holds them to it.
7
7
  * Nothing else in the running code knows which release it is.
8
8
  */
9
- export declare const VERSION = "0.149.1";
9
+ export declare const VERSION = "0.151.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.149.1';
1
+ export const VERSION = '0.151.0';
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.149.1",
4
+ "version": "0.151.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -56,11 +56,11 @@
56
56
  "bugs": "https://github.com/project-fusion/fusion/issues",
57
57
  "dependencies": {
58
58
  "@floating-ui/dom": "^1.8.0",
59
- "@fusion.dev/helix-icons": "0.149.1"
59
+ "@fusion.dev/helix-icons": "0.151.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@fusion.dev/helix-cem": "0.149.1",
63
- "@fusion.dev/helix-testing": "0.149.1",
62
+ "@fusion.dev/helix-cem": "0.151.0",
63
+ "@fusion.dev/helix-testing": "0.151.0",
64
64
  "@lit-labs/ssr": "^4.1.0",
65
65
  "lit": "^3.2.1"
66
66
  },