@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.
- package/custom-elements.json +482 -8
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +62 -5
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +112 -12
- package/dist/events/details.d.ts +14 -0
- package/dist/events/events.d.ts +6 -2
- package/dist/events/events.js +5 -1
- package/dist/events/names.js +2 -1
- package/dist/events/reading-change.d.ts +14 -0
- package/dist/events/reading-change.js +11 -0
- package/dist/events/reading-press.d.ts +13 -0
- package/dist/events/reading-press.js +11 -0
- package/dist/helix-element/helix-element.js +2 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/mixins/mixin-collection/mixin-collection.d.ts +5 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +7 -1
- package/dist/mixins/mixin-picker-editing/mixin-picker-editing.js +34 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +3 -2
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +1 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +8 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +73 -0
- package/dist/utils/chord/key-names.d.ts +14 -2
- package/dist/utils/chord/key-names.js +58 -3
- package/dist/utils/date-segments/date-segments.d.ts +5 -4
- package/dist/utils/date-segments/date-segments.js +12 -0
- package/dist/utils/draw-value-axis/draw-value-axis.d.ts +22 -0
- package/dist/utils/draw-value-axis/draw-value-axis.js +9 -0
- package/dist/utils/read-color/read-color.d.ts +24 -8
- package/dist/utils/read-color/read-color.js +20 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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,
|
|
66
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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,
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* says the segment is full and
|
|
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
|
-
*
|
|
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
|
-
* **
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
5
|
-
context.fillStyle =
|
|
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
|
|
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
|
|
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
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
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.
|
|
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.
|
|
59
|
+
"@fusion.dev/helix-icons": "0.151.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@fusion.dev/helix-cem": "0.
|
|
63
|
-
"@fusion.dev/helix-testing": "0.
|
|
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
|
},
|