@santi020k/lumen-core 3.0.0 → 4.0.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/PHONE_FLAG_LICENSE.txt +21 -0
- package/README.md +379 -4
- package/THIRD_PARTY_NOTICES.md +16 -2
- package/dist/ai-surfaces.d.ts +25 -0
- package/dist/ai-surfaces.d.ts.map +1 -0
- package/dist/ai-surfaces.js +162 -0
- package/dist/amount-field.d.ts +22 -0
- package/dist/amount-field.d.ts.map +1 -0
- package/dist/amount-field.js +207 -0
- package/dist/attachments.d.ts +15 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +123 -0
- package/dist/bullet-chart.d.ts +33 -0
- package/dist/bullet-chart.d.ts.map +1 -0
- package/dist/bullet-chart.js +80 -0
- package/dist/chart-activation.d.ts +32 -0
- package/dist/chart-activation.d.ts.map +1 -0
- package/dist/chart-activation.js +97 -0
- package/dist/chart-interaction.d.ts +12 -0
- package/dist/chart-interaction.d.ts.map +1 -0
- package/dist/chart-interaction.js +274 -0
- package/dist/chart-models.d.ts +156 -0
- package/dist/chart-models.d.ts.map +1 -0
- package/dist/chart-models.js +348 -0
- package/dist/chart-motion.d.ts +6 -0
- package/dist/chart-motion.d.ts.map +1 -0
- package/dist/chart-motion.js +107 -0
- package/dist/charts.d.ts +68 -3
- package/dist/charts.d.ts.map +1 -1
- package/dist/charts.js +328 -29
- package/dist/code.js +1 -1
- package/dist/combobox.d.ts +7 -0
- package/dist/combobox.d.ts.map +1 -0
- package/dist/combobox.js +253 -0
- package/dist/comparison-chart.d.ts +32 -0
- package/dist/comparison-chart.d.ts.map +1 -0
- package/dist/comparison-chart.js +64 -0
- package/dist/components.d.ts +210 -25
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +102 -26
- package/dist/dashboard.d.ts +16 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +21 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +21 -5
- package/dist/dates.d.ts +13 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +37 -0
- package/dist/device-frame.d.ts +25 -0
- package/dist/device-frame.d.ts.map +1 -0
- package/dist/device-frame.js +50 -0
- package/dist/direction.d.ts +3 -0
- package/dist/direction.d.ts.map +1 -0
- package/dist/direction.js +10 -0
- package/dist/extended-charts.d.ts +95 -0
- package/dist/extended-charts.d.ts.map +1 -0
- package/dist/extended-charts.js +175 -0
- package/dist/figma.d.ts.map +1 -1
- package/dist/figma.js +10 -1
- package/dist/forms.d.ts.map +1 -1
- package/dist/forms.js +18 -4
- package/dist/icon-data.generated.d.ts +1 -1
- package/dist/icon-data.generated.d.ts.map +1 -1
- package/dist/icon-data.generated.js +1 -1
- package/dist/icons.js +3 -4
- package/dist/illustrations.generated.d.ts +1 -1
- package/dist/image-comparison.d.ts +14 -0
- package/dist/image-comparison.d.ts.map +1 -0
- package/dist/image-comparison.js +48 -0
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -3
- package/dist/media-viewport.d.ts +24 -0
- package/dist/media-viewport.d.ts.map +1 -0
- package/dist/media-viewport.js +119 -0
- package/dist/media-workspace.d.ts +27 -0
- package/dist/media-workspace.d.ts.map +1 -0
- package/dist/media-workspace.js +83 -0
- package/dist/message-scroller.d.ts +14 -0
- package/dist/message-scroller.d.ts.map +1 -0
- package/dist/message-scroller.js +113 -0
- package/dist/motion-workflows.d.ts +24 -0
- package/dist/motion-workflows.d.ts.map +1 -0
- package/dist/motion-workflows.js +220 -0
- package/dist/motion.d.ts +12 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/motion.js +96 -0
- package/dist/native-input.d.ts +25 -0
- package/dist/native-input.d.ts.map +1 -0
- package/dist/native-input.js +188 -0
- package/dist/phone-flags.generated.d.ts +2 -0
- package/dist/phone-flags.generated.d.ts.map +1 -0
- package/dist/phone-flags.generated.js +263 -0
- package/dist/phone.d.ts +4 -0
- package/dist/phone.d.ts.map +1 -1
- package/dist/phone.js +6 -1
- package/dist/rich-text.d.ts +7 -0
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +13 -0
- package/dist/rive.d.ts +37 -0
- package/dist/rive.d.ts.map +1 -0
- package/dist/rive.js +129 -0
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +63 -23
- package/dist/tab-indicator.d.ts +3 -0
- package/dist/tab-indicator.d.ts.map +1 -0
- package/dist/tab-indicator.js +57 -0
- package/dist/theme-audit.d.ts +16 -0
- package/dist/theme-audit.d.ts.map +1 -0
- package/dist/theme-audit.js +57 -0
- package/dist/theme-builder.d.ts +7 -1
- package/dist/theme-builder.d.ts.map +1 -1
- package/dist/theme-builder.js +45 -23
- package/dist/theme-presets.generated.d.ts +217 -0
- package/dist/theme-presets.generated.d.ts.map +1 -0
- package/dist/theme-presets.generated.js +231 -0
- package/dist/theme.d.ts +11 -3
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +56 -6
- package/dist/virtual-collection.d.ts +14 -0
- package/dist/virtual-collection.d.ts.map +1 -0
- package/dist/virtual-collection.js +132 -0
- package/dist/virtual-list.d.ts +7 -0
- package/dist/virtual-list.d.ts.map +1 -0
- package/dist/virtual-list.js +135 -0
- package/dist/virtual-window.d.ts +19 -0
- package/dist/virtual-window.d.ts.map +1 -0
- package/dist/virtual-window.js +84 -0
- package/dist/visual-effects.d.ts +6 -0
- package/dist/visual-effects.d.ts.map +1 -0
- package/dist/visual-effects.js +44 -0
- package/dist/world-map-data.generated.d.ts +9 -0
- package/dist/world-map-data.generated.d.ts.map +1 -0
- package/dist/world-map-data.generated.js +240 -0
- package/dist/world-map-navigation.d.ts +16 -0
- package/dist/world-map-navigation.d.ts.map +1 -0
- package/dist/world-map-navigation.js +53 -0
- package/dist/world-map-pan.d.ts +3 -0
- package/dist/world-map-pan.d.ts.map +1 -0
- package/dist/world-map-pan.js +66 -0
- package/dist/world-map-zoom.d.ts +13 -0
- package/dist/world-map-zoom.d.ts.map +1 -0
- package/dist/world-map-zoom.js +61 -0
- package/dist/world-map.d.ts +71 -0
- package/dist/world-map.d.ts.map +1 -0
- package/dist/world-map.js +143 -0
- package/package.json +51 -4
package/dist/combobox.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
let nextId = 0;
|
|
2
|
+
const ignoresKeyboard = (event, composing) => event.defaultPrevented || event.isComposing || composing ||
|
|
3
|
+
event.altKey || event.ctrlKey || event.metaKey;
|
|
4
|
+
const filterOption = (item, query, filterHidden, writeHidden) => {
|
|
5
|
+
const label = item.textContent.trim().toLowerCase();
|
|
6
|
+
const value = item.dataset.value?.toLowerCase() ?? '';
|
|
7
|
+
const hidden = Boolean(query) && !label.includes(query) && !value.includes(query);
|
|
8
|
+
if (hidden && !item.hidden) {
|
|
9
|
+
writeHidden(item, true);
|
|
10
|
+
}
|
|
11
|
+
else if (!hidden && filterHidden.has(item)) {
|
|
12
|
+
writeHidden(item, false);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Editable combobox. */
|
|
16
|
+
export const createLumenComboboxController = (root) => {
|
|
17
|
+
const input = root.querySelector('input[role="combobox"]');
|
|
18
|
+
const list = root.querySelector('[role="listbox"]');
|
|
19
|
+
const document = root.ownerDocument;
|
|
20
|
+
const view = document.defaultView;
|
|
21
|
+
if (!input || !list || !view)
|
|
22
|
+
return { close: () => undefined, destroy: () => undefined };
|
|
23
|
+
const abort = new view.AbortController();
|
|
24
|
+
const { signal } = abort;
|
|
25
|
+
let active;
|
|
26
|
+
let composing = false;
|
|
27
|
+
let filtering = false;
|
|
28
|
+
let destroyed = false;
|
|
29
|
+
const filterHidden = new Set();
|
|
30
|
+
let resetTimer;
|
|
31
|
+
const eventRoot = root.getRootNode();
|
|
32
|
+
const observerRef = {};
|
|
33
|
+
const preserveVisibility = (records) => {
|
|
34
|
+
for (const record of records) {
|
|
35
|
+
if (record.attributeName === 'hidden') {
|
|
36
|
+
for (const option of filterHidden)
|
|
37
|
+
if (option === record.target)
|
|
38
|
+
filterHidden.delete(option);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const option of filterHidden) {
|
|
42
|
+
if (!list.contains(option)) {
|
|
43
|
+
option.hidden = false;
|
|
44
|
+
filterHidden.delete(option);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (!list.id)
|
|
49
|
+
list.id = `ui-combobox-list-${++nextId}`;
|
|
50
|
+
input.setAttribute('aria-controls', list.id);
|
|
51
|
+
input.setAttribute('aria-autocomplete', 'list');
|
|
52
|
+
const items = () => [...list.querySelectorAll('[role="option"]')];
|
|
53
|
+
const editable = () => !input.matches(':disabled') && !input.readOnly;
|
|
54
|
+
const available = (item) => !item.hidden &&
|
|
55
|
+
!item.matches(':disabled, [disabled]') && item.getAttribute('aria-disabled') !== 'true';
|
|
56
|
+
const activate = (item) => {
|
|
57
|
+
active = item;
|
|
58
|
+
for (const option of items())
|
|
59
|
+
option.setAttribute('aria-selected', String(option === item));
|
|
60
|
+
if (item) {
|
|
61
|
+
if (!item.id)
|
|
62
|
+
item.id = `ui-combobox-option-${++nextId}`;
|
|
63
|
+
input.setAttribute('aria-activedescendant', item.id);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
input.removeAttribute('aria-activedescendant');
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const close = () => {
|
|
70
|
+
input.setAttribute('aria-expanded', 'false');
|
|
71
|
+
if (!list.hidden)
|
|
72
|
+
list.hidden = true;
|
|
73
|
+
list.dataset.state = 'closed';
|
|
74
|
+
activate();
|
|
75
|
+
};
|
|
76
|
+
const flushVisibility = () => {
|
|
77
|
+
preserveVisibility(observerRef.current?.takeRecords() ?? []);
|
|
78
|
+
};
|
|
79
|
+
const writeHidden = (item, hidden) => {
|
|
80
|
+
flushVisibility();
|
|
81
|
+
if (hidden)
|
|
82
|
+
filterHidden.add(item);
|
|
83
|
+
else
|
|
84
|
+
filterHidden.delete(item);
|
|
85
|
+
item.hidden = hidden;
|
|
86
|
+
preserveVisibility(observerRef.current?.takeRecords().slice(1) ?? []);
|
|
87
|
+
};
|
|
88
|
+
const refresh = () => {
|
|
89
|
+
flushVisibility();
|
|
90
|
+
const query = input.value.trim().toLowerCase();
|
|
91
|
+
for (const item of items()) {
|
|
92
|
+
item.tabIndex = -1;
|
|
93
|
+
if (!item.id)
|
|
94
|
+
item.id = `ui-combobox-option-${++nextId}`;
|
|
95
|
+
if (filtering)
|
|
96
|
+
filterOption(item, query, filterHidden, writeHidden);
|
|
97
|
+
}
|
|
98
|
+
if (active && (!list.contains(active) || !available(active)))
|
|
99
|
+
activate();
|
|
100
|
+
if (!editable())
|
|
101
|
+
close();
|
|
102
|
+
};
|
|
103
|
+
const open = () => {
|
|
104
|
+
if (!editable())
|
|
105
|
+
return;
|
|
106
|
+
refresh();
|
|
107
|
+
input.setAttribute('aria-expanded', 'true');
|
|
108
|
+
list.hidden = false;
|
|
109
|
+
list.dataset.state = 'open';
|
|
110
|
+
};
|
|
111
|
+
const select = (item) => {
|
|
112
|
+
if (!editable() || !list.contains(item) || !available(item))
|
|
113
|
+
return;
|
|
114
|
+
input.value = item.dataset.value ?? item.textContent.trim();
|
|
115
|
+
input.dispatchEvent(new view.Event('input', { bubbles: true }));
|
|
116
|
+
input.dispatchEvent(new view.Event('change', { bubbles: true }));
|
|
117
|
+
input.focus({ preventScroll: true });
|
|
118
|
+
close();
|
|
119
|
+
};
|
|
120
|
+
const navigate = (key) => {
|
|
121
|
+
open();
|
|
122
|
+
const options = items().filter(available);
|
|
123
|
+
const index = active ? options.indexOf(active) : -1;
|
|
124
|
+
let next = (index + 1) % options.length;
|
|
125
|
+
if (key === 'ArrowUp') {
|
|
126
|
+
next = index < 0 ? options.length - 1 : (index - 1 + options.length) % options.length;
|
|
127
|
+
}
|
|
128
|
+
activate(options[next]);
|
|
129
|
+
if (typeof active?.scrollIntoView === 'function')
|
|
130
|
+
active.scrollIntoView({ block: 'nearest' });
|
|
131
|
+
};
|
|
132
|
+
const keydown = (event) => {
|
|
133
|
+
if (ignoresKeyboard(event, composing) || !editable())
|
|
134
|
+
return;
|
|
135
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
navigate(event.key);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
switch (event.key) {
|
|
141
|
+
case 'Escape':
|
|
142
|
+
if (list.hidden)
|
|
143
|
+
return;
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
close();
|
|
146
|
+
break;
|
|
147
|
+
case 'Enter':
|
|
148
|
+
if (list.hidden || !active)
|
|
149
|
+
return;
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
select(active);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const optionFromEvent = (event) => (items().find(option => event.composedPath().includes(option)));
|
|
156
|
+
refresh();
|
|
157
|
+
close();
|
|
158
|
+
input.addEventListener('focus', open, { signal });
|
|
159
|
+
input.addEventListener('compositionstart', () => {
|
|
160
|
+
composing = true;
|
|
161
|
+
}, { signal });
|
|
162
|
+
input.addEventListener('compositionend', () => {
|
|
163
|
+
composing = false;
|
|
164
|
+
filtering = true;
|
|
165
|
+
activate();
|
|
166
|
+
open();
|
|
167
|
+
}, { signal });
|
|
168
|
+
input.addEventListener('input', () => {
|
|
169
|
+
if (composing)
|
|
170
|
+
return;
|
|
171
|
+
filtering = true;
|
|
172
|
+
activate();
|
|
173
|
+
open();
|
|
174
|
+
}, { signal });
|
|
175
|
+
input.addEventListener('keydown', keydown, { signal });
|
|
176
|
+
eventRoot.addEventListener('reset', event => {
|
|
177
|
+
if (event.target !== input.form)
|
|
178
|
+
return;
|
|
179
|
+
view.clearTimeout(resetTimer);
|
|
180
|
+
resetTimer = view.setTimeout(() => {
|
|
181
|
+
if (destroyed || event.defaultPrevented || !input.isConnected)
|
|
182
|
+
return;
|
|
183
|
+
composing = false;
|
|
184
|
+
filtering = true;
|
|
185
|
+
close();
|
|
186
|
+
refresh();
|
|
187
|
+
});
|
|
188
|
+
}, { capture: true, signal });
|
|
189
|
+
list.addEventListener('pointerdown', event => {
|
|
190
|
+
if (optionFromEvent(event))
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
}, { signal });
|
|
193
|
+
list.addEventListener('click', event => {
|
|
194
|
+
if (event.defaultPrevented)
|
|
195
|
+
return;
|
|
196
|
+
const option = optionFromEvent(event);
|
|
197
|
+
if (option) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
select(option);
|
|
200
|
+
}
|
|
201
|
+
}, { signal });
|
|
202
|
+
const isNode = (target) => {
|
|
203
|
+
try {
|
|
204
|
+
view.Node.prototype.contains.call(target, null);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
root.addEventListener('focusout', event => {
|
|
212
|
+
if (!event.relatedTarget || !isNode(event.relatedTarget) || !root.contains(event.relatedTarget))
|
|
213
|
+
close();
|
|
214
|
+
}, { signal });
|
|
215
|
+
document.addEventListener('pointerdown', event => {
|
|
216
|
+
if (!event.composedPath().includes(root))
|
|
217
|
+
close();
|
|
218
|
+
}, { signal });
|
|
219
|
+
const observer = new view.MutationObserver(records => {
|
|
220
|
+
preserveVisibility(records);
|
|
221
|
+
if (!destroyed)
|
|
222
|
+
refresh();
|
|
223
|
+
});
|
|
224
|
+
observerRef.current = observer;
|
|
225
|
+
observer.observe(list, {
|
|
226
|
+
attributeFilter: ['disabled', 'aria-disabled', 'data-value', 'hidden'],
|
|
227
|
+
attributes: true,
|
|
228
|
+
characterData: true,
|
|
229
|
+
childList: true,
|
|
230
|
+
subtree: true
|
|
231
|
+
});
|
|
232
|
+
const disabledObserver = new view.MutationObserver(records => {
|
|
233
|
+
if (records.some(record => record.target.nodeName === 'FIELDSET' && record.target.contains(input)))
|
|
234
|
+
refresh();
|
|
235
|
+
});
|
|
236
|
+
disabledObserver.observe(eventRoot, { attributeFilter: ['disabled'], attributes: true, subtree: true });
|
|
237
|
+
observer.observe(input, { attributeFilter: ['disabled', 'readonly'], attributes: true });
|
|
238
|
+
return {
|
|
239
|
+
close,
|
|
240
|
+
destroy: () => {
|
|
241
|
+
destroyed = true;
|
|
242
|
+
view.clearTimeout(resetTimer);
|
|
243
|
+
abort.abort();
|
|
244
|
+
flushVisibility();
|
|
245
|
+
observer.disconnect();
|
|
246
|
+
disabledObserver.disconnect();
|
|
247
|
+
for (const option of filterHidden)
|
|
248
|
+
option.hidden = false;
|
|
249
|
+
filterHidden.clear();
|
|
250
|
+
close();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LumenChartDomain, LumenChartTone } from './charts.js';
|
|
2
|
+
export interface LumenComparisonDatum {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
value: number | null;
|
|
6
|
+
reference?: number | null;
|
|
7
|
+
tone?: LumenChartTone;
|
|
8
|
+
}
|
|
9
|
+
export interface LumenComparisonOptions {
|
|
10
|
+
domain?: LumenChartDomain | undefined;
|
|
11
|
+
paired?: boolean | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Validate a complete comparison dataset; never silently drop invalid rows. */
|
|
14
|
+
export declare const isLumenComparisonDatum: (value: unknown) => value is LumenComparisonDatum;
|
|
15
|
+
/** Horizontal rankings and paired comparisons on one zero-inclusive scale. */
|
|
16
|
+
export declare const createLumenComparisonGeometry: (data: readonly LumenComparisonDatum[], options?: LumenComparisonOptions) => {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
domain: LumenChartDomain;
|
|
19
|
+
ticks: number[];
|
|
20
|
+
rows: {
|
|
21
|
+
reference: number | null;
|
|
22
|
+
valuePosition: number | null;
|
|
23
|
+
referencePosition: number | null;
|
|
24
|
+
start: number;
|
|
25
|
+
width: number;
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
value: number | null;
|
|
29
|
+
tone?: LumenChartTone;
|
|
30
|
+
}[];
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=comparison-chart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparison-chart.d.ts","sourceRoot":"","sources":["../src/comparison-chart.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGnE,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAA;IACrC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC7B;AAkBD,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,oBAOhE,CAAA;AAED,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,GACxC,MAAM,SAAS,oBAAoB,EAAE,EAAE,UAAS,sBAA2B;;;;;;;;;;YAxCvE,MAAM;eACH,MAAM;eACN,MAAM,GAAG,IAAI;eAEb,cAAc;;CAqFtB,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { lumenChartTones, scaleLumenChartValue } from './charts.js';
|
|
2
|
+
const validMeasurement = (value) => value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
3
|
+
const validLabel = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
4
|
+
const validReference = (value) => value === undefined || validMeasurement(value);
|
|
5
|
+
const validTone = (value) => value === undefined || lumenChartTones.some(tone => tone === value);
|
|
6
|
+
const encloses = (domain, min, max) => (Number.isFinite(domain.min) && Number.isFinite(domain.max) &&
|
|
7
|
+
domain.min < domain.max && domain.min <= min && domain.max >= max);
|
|
8
|
+
const optionalFieldsValid = (value) => {
|
|
9
|
+
if (typeof value !== 'object' || value === null)
|
|
10
|
+
return false;
|
|
11
|
+
return (!('reference' in value) || validReference(value.reference)) && (!('tone' in value) || validTone(value.tone));
|
|
12
|
+
};
|
|
13
|
+
/** Validate a complete comparison dataset; never silently drop invalid rows. */
|
|
14
|
+
export const isLumenComparisonDatum = (value) => {
|
|
15
|
+
if (typeof value !== 'object' || value === null)
|
|
16
|
+
return false;
|
|
17
|
+
if (!('id' in value) || !('label' in value) || !('value' in value))
|
|
18
|
+
return false;
|
|
19
|
+
return validLabel(value.id) && validLabel(value.label) && validMeasurement(value.value) &&
|
|
20
|
+
optionalFieldsValid(value);
|
|
21
|
+
};
|
|
22
|
+
/** Horizontal rankings and paired comparisons on one zero-inclusive scale. */
|
|
23
|
+
export const createLumenComparisonGeometry = (data, options = {}) => {
|
|
24
|
+
const entries = data;
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
let min = 0;
|
|
27
|
+
let max = 0;
|
|
28
|
+
const validData = Array.isArray(data) && Array.from(entries).every(item => {
|
|
29
|
+
if (!isLumenComparisonDatum(item) || ids.has(item.id))
|
|
30
|
+
return false;
|
|
31
|
+
ids.add(item.id);
|
|
32
|
+
for (const value of [item.value, ...(options.paired ? [item.reference ?? null] : [])]) {
|
|
33
|
+
if (value !== null) {
|
|
34
|
+
min = Math.min(min, value);
|
|
35
|
+
max = Math.max(max, value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
const requested = options.domain ?? { min, max: max === min ? max + 1 : max };
|
|
41
|
+
const valid = validData && encloses(requested, min, max);
|
|
42
|
+
const domain = valid ? requested : { min: 0, max: 1 };
|
|
43
|
+
const position = (value) => scaleLumenChartValue(value, domain, 0, 1);
|
|
44
|
+
return {
|
|
45
|
+
valid,
|
|
46
|
+
domain,
|
|
47
|
+
ticks: [domain.min, domain.min / 2 + domain.max / 2, domain.max],
|
|
48
|
+
rows: valid ?
|
|
49
|
+
entries.map(item => {
|
|
50
|
+
const reference = options.paired ? item.reference ?? null : 0;
|
|
51
|
+
const valuePosition = item.value === null ? null : position(item.value);
|
|
52
|
+
const referencePosition = reference === null ? null : position(reference);
|
|
53
|
+
return {
|
|
54
|
+
...item,
|
|
55
|
+
reference,
|
|
56
|
+
valuePosition,
|
|
57
|
+
referencePosition,
|
|
58
|
+
start: Math.min(valuePosition ?? 0, referencePosition ?? 0),
|
|
59
|
+
width: valuePosition === null || referencePosition === null ? 0 : Math.abs(valuePosition - referencePosition)
|
|
60
|
+
};
|
|
61
|
+
}) :
|
|
62
|
+
[]
|
|
63
|
+
};
|
|
64
|
+
};
|