@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export const readLumenPromptSubmitDetail = (value) => {
|
|
2
|
+
if (typeof value === 'object' && value !== null && 'text' in value && typeof value.text === 'string')
|
|
3
|
+
return { text: value.text };
|
|
4
|
+
return undefined;
|
|
5
|
+
};
|
|
6
|
+
export const readLumenApprovalResponseDetail = (value) => {
|
|
7
|
+
if (typeof value !== 'object' || value === null || !('requestId' in value) || typeof value.requestId !== 'string' || !('response' in value))
|
|
8
|
+
return undefined;
|
|
9
|
+
if (value.response === 'approve' || value.response === 'reject')
|
|
10
|
+
return { requestId: value.requestId, response: value.response };
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
13
|
+
export const normalizeLumenPromptLimit = (value) => {
|
|
14
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1)
|
|
15
|
+
return 4000;
|
|
16
|
+
return Math.min(2147483647, Math.floor(value));
|
|
17
|
+
};
|
|
18
|
+
export const canSubmitLumenPrompt = (text, limit = 4000) => (text.trim().length > 0 && text.length <= normalizeLumenPromptLimit(limit));
|
|
19
|
+
const hasUnsafeCitationCharacter = (value) => {
|
|
20
|
+
for (let index = 0; index < value.length; index++) {
|
|
21
|
+
const code = value.charCodeAt(index);
|
|
22
|
+
if (code <= 32 || code === 127 || code === 92)
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
const webCitationHref = (value) => {
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(value);
|
|
30
|
+
if ((url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password)
|
|
31
|
+
return url.href;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
/** Citations accept safe web destinations and local absolute paths, never executable schemes. */
|
|
39
|
+
export const resolveLumenCitationHref = (value) => {
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
return undefined;
|
|
42
|
+
if (hasUnsafeCitationCharacter(value))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (value.startsWith('/') && !value.startsWith('//'))
|
|
45
|
+
return value;
|
|
46
|
+
return webCitationHref(value);
|
|
47
|
+
};
|
|
48
|
+
const isPlainSubmitKey = (event) => event.key === 'Enter' && !event.defaultPrevented && !event.isComposing &&
|
|
49
|
+
!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
50
|
+
/** Bind an event bridge. A supplied native action keeps its normal POST behavior. */
|
|
51
|
+
export const createLumenPromptComposerController = (root) => {
|
|
52
|
+
const form = root.matches('form') ? root : root.querySelector('form');
|
|
53
|
+
const input = root.querySelector('[data-ui-prompt-input]');
|
|
54
|
+
const submit = root.querySelector('[data-ui-prompt-send]');
|
|
55
|
+
const stop = root.querySelector('[data-ui-prompt-stop]');
|
|
56
|
+
const view = root.ownerDocument.defaultView;
|
|
57
|
+
if (!form || !input || !submit || !view)
|
|
58
|
+
return { destroy: () => undefined, sync: () => undefined };
|
|
59
|
+
const sync = () => {
|
|
60
|
+
const pending = root.dataset.pending === 'true';
|
|
61
|
+
const disabled = root.dataset.disabled === 'true';
|
|
62
|
+
input.disabled = disabled;
|
|
63
|
+
const limit = input.maxLength > 0 ? input.maxLength : 4000;
|
|
64
|
+
submit.disabled = disabled || pending || !canSubmitLumenPrompt(input.value, limit);
|
|
65
|
+
if (stop) {
|
|
66
|
+
stop.hidden = !pending;
|
|
67
|
+
stop.disabled = disabled;
|
|
68
|
+
}
|
|
69
|
+
form.setAttribute('aria-busy', String(pending));
|
|
70
|
+
};
|
|
71
|
+
const send = (event) => {
|
|
72
|
+
sync();
|
|
73
|
+
if (submit.disabled) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const detail = { text: input.value.trim() };
|
|
78
|
+
const accepted = root.dispatchEvent(new view.CustomEvent('ui:prompt-submit', { bubbles: true, cancelable: true, detail }));
|
|
79
|
+
if (!accepted || !form.hasAttribute('action'))
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
};
|
|
82
|
+
const keydown = (event) => {
|
|
83
|
+
if (!isPlainSubmitKey(event))
|
|
84
|
+
return;
|
|
85
|
+
if (root.dataset.submitOnEnter !== 'true')
|
|
86
|
+
return;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
sync();
|
|
89
|
+
if (!submit.disabled && form instanceof view.HTMLFormElement)
|
|
90
|
+
form.requestSubmit(submit);
|
|
91
|
+
};
|
|
92
|
+
const stopRequest = () => {
|
|
93
|
+
if (root.dataset.pending !== 'true' || root.dataset.disabled === 'true')
|
|
94
|
+
return;
|
|
95
|
+
root.dispatchEvent(new view.CustomEvent('ui:prompt-stop', { bubbles: true }));
|
|
96
|
+
};
|
|
97
|
+
input.addEventListener('input', sync);
|
|
98
|
+
let resetTimer;
|
|
99
|
+
const reset = (event) => {
|
|
100
|
+
if (resetTimer !== undefined)
|
|
101
|
+
view.clearTimeout(resetTimer);
|
|
102
|
+
resetTimer = view.setTimeout(() => {
|
|
103
|
+
resetTimer = undefined;
|
|
104
|
+
if (!event.defaultPrevented)
|
|
105
|
+
sync();
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
input.addEventListener('keydown', keydown);
|
|
109
|
+
form.addEventListener('submit', send);
|
|
110
|
+
form.addEventListener('reset', reset);
|
|
111
|
+
stop?.addEventListener('click', stopRequest);
|
|
112
|
+
const observer = new view.MutationObserver(sync);
|
|
113
|
+
observer.observe(root, { attributeFilter: ['data-pending', 'data-disabled'], attributes: true });
|
|
114
|
+
sync();
|
|
115
|
+
return {
|
|
116
|
+
destroy: () => {
|
|
117
|
+
observer.disconnect();
|
|
118
|
+
input.removeEventListener('input', sync);
|
|
119
|
+
input.removeEventListener('keydown', keydown);
|
|
120
|
+
form.removeEventListener('submit', send);
|
|
121
|
+
form.removeEventListener('reset', reset);
|
|
122
|
+
if (resetTimer !== undefined)
|
|
123
|
+
view.clearTimeout(resetTimer);
|
|
124
|
+
stop?.removeEventListener('click', stopRequest);
|
|
125
|
+
},
|
|
126
|
+
sync
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
/** This event requests a decision; the application must still authorize the actual operation. */
|
|
130
|
+
export const bindLumenApprovalCard = (root) => {
|
|
131
|
+
const view = root.ownerDocument.defaultView;
|
|
132
|
+
if (!view)
|
|
133
|
+
return () => undefined;
|
|
134
|
+
const sync = () => {
|
|
135
|
+
const disabled = root.dataset.disabled === 'true' || root.dataset.status !== 'pending' || !root.dataset.requestId;
|
|
136
|
+
for (const button of root.querySelectorAll('button[data-ui-approval-response]')) {
|
|
137
|
+
if (button.closest('[data-ui-approval-card]') === root)
|
|
138
|
+
button.disabled = disabled;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const observer = new view.MutationObserver(sync);
|
|
142
|
+
observer.observe(root, { attributeFilter: ['data-disabled', 'data-status', 'data-request-id'], attributes: true });
|
|
143
|
+
sync();
|
|
144
|
+
const click = (event) => {
|
|
145
|
+
if (!(event.target instanceof view.Element) || event.defaultPrevented || root.dataset.status !== 'pending')
|
|
146
|
+
return;
|
|
147
|
+
const button = event.target.closest('button[data-ui-approval-response]');
|
|
148
|
+
if (!button || button.disabled || button.closest('[data-ui-approval-card]') !== root)
|
|
149
|
+
return;
|
|
150
|
+
const requestId = root.dataset.requestId;
|
|
151
|
+
const response = button.dataset.uiApprovalResponse;
|
|
152
|
+
if (!requestId || (response !== 'approve' && response !== 'reject'))
|
|
153
|
+
return;
|
|
154
|
+
const detail = { requestId, response };
|
|
155
|
+
root.dispatchEvent(new view.CustomEvent('ui:approval-response', { bubbles: true, cancelable: true, detail }));
|
|
156
|
+
};
|
|
157
|
+
root.addEventListener('click', click);
|
|
158
|
+
return () => {
|
|
159
|
+
observer.disconnect();
|
|
160
|
+
root.removeEventListener('click', click);
|
|
161
|
+
};
|
|
162
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface LumenAmountOptions {
|
|
2
|
+
locale?: string;
|
|
3
|
+
fractionDigits?: number;
|
|
4
|
+
allowNegative?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface LumenAmountChangeDetail {
|
|
7
|
+
/** Editable ASCII decimal draft; intermediate '-' and trailing decimal points are retained. */
|
|
8
|
+
draft: string;
|
|
9
|
+
/** Complete decimal string. Undefined for an empty or intermediate draft. */
|
|
10
|
+
value: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare const getLumenAmountValue: (draft: string) => string | undefined;
|
|
13
|
+
export declare const formatLumenAmountDraft: (draft: string, options?: LumenAmountOptions) => string;
|
|
14
|
+
export declare const parseLumenAmountDraft: (source: string, options?: LumenAmountOptions, strictGrouping?: boolean) => string | undefined;
|
|
15
|
+
export interface LumenAmountFieldController {
|
|
16
|
+
destroy: () => void;
|
|
17
|
+
/** Rebind reset handling after moving the existing control to another document or shadow root. */
|
|
18
|
+
refresh: () => void;
|
|
19
|
+
setValue: (draft: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const createLumenAmountFieldController: (root: HTMLElement, onChange?: (detail: LumenAmountChangeDetail) => void) => LumenAmountFieldController;
|
|
22
|
+
//# sourceMappingURL=amount-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"amount-field.d.ts","sourceRoot":"","sources":["../src/amount-field.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,+FAA+F;IAC/F,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1B;AAqDD,eAAO,MAAM,mBAAmB,GAAI,OAAO,MAAM,KAAG,MAAM,GAAG,SAI5D,CAAA;AAED,eAAO,MAAM,sBAAsB,GAAI,OAAO,MAAM,EAAE,UAAS,kBAAuB,KAAG,MAcxF,CAAA;AAgDD,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EAAE,UAAS,kBAAuB,EAAE,wBAAqB,KACtE,MAAM,GAAG,SAYX,CAAA;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,kGAAkG;IAClG,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAeD,eAAO,MAAM,gCAAgC,GAC3C,MAAM,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,uBAAuB,KAAK,IAAI,KACtE,0BAsIF,CAAA"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const amountLimit = 1024;
|
|
2
|
+
const validateFractionDigits = (digits) => {
|
|
3
|
+
if (!Number.isInteger(digits) || digits < 0 || digits > 20) {
|
|
4
|
+
throw new RangeError('Amount fractionDigits must be an integer from 0 to 20.');
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
const amountFormat = (options) => {
|
|
8
|
+
const fractionDigits = options.fractionDigits ?? 2;
|
|
9
|
+
let locale = options.locale ?? 'en-US';
|
|
10
|
+
validateFractionDigits(fractionDigits);
|
|
11
|
+
let formatter;
|
|
12
|
+
try {
|
|
13
|
+
formatter = new Intl.NumberFormat(locale, { useGrouping: true, maximumFractionDigits: 0 });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
locale = 'en-US';
|
|
17
|
+
formatter = new Intl.NumberFormat(locale, { useGrouping: true, maximumFractionDigits: 0 });
|
|
18
|
+
}
|
|
19
|
+
const decimal = new Intl.NumberFormat(locale).formatToParts(1.1).find(part => part.type === 'decimal')?.value ?? '.';
|
|
20
|
+
const group = formatter.formatToParts(1000000).find(part => part.type === 'group')?.value;
|
|
21
|
+
const minus = formatter.formatToParts(-1).find(part => part.type === 'minusSign')?.value ?? '-';
|
|
22
|
+
const digitFormatter = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
23
|
+
const digits = Array.from({ length: 10 }, (_, digit) => digitFormatter.format(digit));
|
|
24
|
+
return { formatter, decimal, group, minus, digits, fractionDigits };
|
|
25
|
+
};
|
|
26
|
+
const validDraft = (draft, options) => {
|
|
27
|
+
if (draft.length > amountLimit)
|
|
28
|
+
return false;
|
|
29
|
+
const { fractionDigits } = amountFormat(options);
|
|
30
|
+
const negative = draft.startsWith('-');
|
|
31
|
+
if (negative && !options.allowNegative)
|
|
32
|
+
return false;
|
|
33
|
+
const unsigned = negative ? draft.slice(1) : draft;
|
|
34
|
+
const pieces = unsigned.split('.');
|
|
35
|
+
if (pieces.length > 2 || (pieces.length === 2 && fractionDigits === 0))
|
|
36
|
+
return false;
|
|
37
|
+
const fraction = pieces[1] ?? '';
|
|
38
|
+
return pieces.every(piece => /^[0-9]*$/u.test(piece)) && fraction.length <= fractionDigits;
|
|
39
|
+
};
|
|
40
|
+
export const getLumenAmountValue = (draft) => {
|
|
41
|
+
if (!/^-?[0-9]+(?:\.[0-9]+)?$/u.test(draft) || draft.length > amountLimit)
|
|
42
|
+
return undefined;
|
|
43
|
+
return draft;
|
|
44
|
+
};
|
|
45
|
+
export const formatLumenAmountDraft = (draft, options = {}) => {
|
|
46
|
+
if (!validDraft(draft, options))
|
|
47
|
+
throw new RangeError('Amount value must be an ASCII decimal draft within the configured precision.');
|
|
48
|
+
if (draft === '')
|
|
49
|
+
return '';
|
|
50
|
+
const { formatter, decimal, minus, digits } = amountFormat(options);
|
|
51
|
+
const negative = draft.startsWith('-');
|
|
52
|
+
const unsigned = negative ? draft.slice(1) : draft;
|
|
53
|
+
const [integer = '', fraction] = unsigned.split('.');
|
|
54
|
+
const formattedInteger = integer ? formatter.format(BigInt(integer)) : '';
|
|
55
|
+
const localizedFraction = Array.from(fraction ?? '', digit => digits[Number(digit)] ?? digit).join('');
|
|
56
|
+
// Use an unsigned formatter so intermediate negative drafts never acquire a rounded sign.
|
|
57
|
+
return `${negative ? minus : ''}${formattedInteger}${fraction !== undefined ? decimal + localizedFraction : ''}`;
|
|
58
|
+
};
|
|
59
|
+
/** Strict grouping for paste; ordinary editing can temporarily disturb existing group separators. */
|
|
60
|
+
const normalizeLeadingDecimal = (draft) => {
|
|
61
|
+
if (draft.startsWith('.'))
|
|
62
|
+
return `0${draft}`;
|
|
63
|
+
return draft.startsWith('-.') ? `-0${draft.slice(1)}` : draft;
|
|
64
|
+
};
|
|
65
|
+
const translateAmountCharacters = (source, options) => {
|
|
66
|
+
const { decimal, group, minus, digits } = amountFormat(options);
|
|
67
|
+
const characters = new Map(digits.map((digit, index) => [digit, String(index)]));
|
|
68
|
+
for (let index = 0; index < 10; index += 1)
|
|
69
|
+
characters.set(String(index), String(index));
|
|
70
|
+
characters.set(decimal, '.');
|
|
71
|
+
characters.set(minus, '-');
|
|
72
|
+
characters.set('-', '-');
|
|
73
|
+
if (group)
|
|
74
|
+
characters.set(group, '');
|
|
75
|
+
for (const mark of ['\u061c', '\u200e', '\u200f'])
|
|
76
|
+
characters.set(mark, '');
|
|
77
|
+
let draft = '';
|
|
78
|
+
for (const character of source.trim()) {
|
|
79
|
+
const translated = characters.get(character);
|
|
80
|
+
if (translated === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
draft += translated;
|
|
83
|
+
}
|
|
84
|
+
return draft;
|
|
85
|
+
};
|
|
86
|
+
const validGrouping = (source, draft, options) => {
|
|
87
|
+
const { decimal, group } = amountFormat(options);
|
|
88
|
+
if (!group || !source.includes(group))
|
|
89
|
+
return true;
|
|
90
|
+
const [integer, fraction = ''] = source.trim().split(decimal);
|
|
91
|
+
return !fraction.includes(group) && integer === formatLumenAmountDraft(draft, options).split(decimal)[0];
|
|
92
|
+
};
|
|
93
|
+
export const parseLumenAmountDraft = (source, options = {}, strictGrouping = true) => {
|
|
94
|
+
if (source.length > amountLimit)
|
|
95
|
+
return undefined;
|
|
96
|
+
const translated = translateAmountCharacters(source, options);
|
|
97
|
+
if (translated === undefined || !validDraft(translated, options))
|
|
98
|
+
return undefined;
|
|
99
|
+
const draft = normalizeLeadingDecimal(translated);
|
|
100
|
+
if (strictGrouping && !validGrouping(source, draft, options))
|
|
101
|
+
return undefined;
|
|
102
|
+
return draft;
|
|
103
|
+
};
|
|
104
|
+
const placeAmountCaret = (input, group, logical) => {
|
|
105
|
+
let caret = 0;
|
|
106
|
+
let count = 0;
|
|
107
|
+
while (caret < input.value.length && count < logical) {
|
|
108
|
+
if (input.value[caret] !== group)
|
|
109
|
+
count += 1;
|
|
110
|
+
caret += 1;
|
|
111
|
+
}
|
|
112
|
+
input.setSelectionRange(caret, caret);
|
|
113
|
+
};
|
|
114
|
+
export const createLumenAmountFieldController = (root, onChange) => {
|
|
115
|
+
const input = root.querySelector('[data-ui-amount-input]');
|
|
116
|
+
const submission = root.querySelector('[data-ui-amount-value]');
|
|
117
|
+
if (!input || !submission)
|
|
118
|
+
throw new Error('AmountField requires visible and submission inputs.');
|
|
119
|
+
const options = {
|
|
120
|
+
locale: root.getAttribute('locale') ?? 'en-US',
|
|
121
|
+
fractionDigits: Number(root.getAttribute('fraction-digits') ?? 2),
|
|
122
|
+
allowNegative: root.hasAttribute('allow-negative')
|
|
123
|
+
};
|
|
124
|
+
const initial = root.getAttribute('default-value') ?? root.getAttribute('value') ?? '';
|
|
125
|
+
let draft = root.getAttribute('value') ?? initial;
|
|
126
|
+
let composing = false;
|
|
127
|
+
let destroyed = false;
|
|
128
|
+
let resetTimer;
|
|
129
|
+
const sync = () => {
|
|
130
|
+
input.value = formatLumenAmountDraft(draft, options);
|
|
131
|
+
submission.value = getLumenAmountValue(draft) ?? '';
|
|
132
|
+
input.setCustomValidity(draft !== '' && getLumenAmountValue(draft) === undefined ? root.getAttribute('invalid-message') ?? 'Enter a complete amount.' : '');
|
|
133
|
+
};
|
|
134
|
+
const group = amountFormat(options).group;
|
|
135
|
+
const logicalCount = (text) => Array.from(text).filter(character => character !== group).length;
|
|
136
|
+
const emitChange = () => {
|
|
137
|
+
const detail = { draft, value: getLumenAmountValue(draft) };
|
|
138
|
+
const EventType = root.ownerDocument.defaultView?.CustomEvent;
|
|
139
|
+
if (EventType)
|
|
140
|
+
root.dispatchEvent(new EventType('ui:amount-change', { bubbles: true, detail }));
|
|
141
|
+
onChange?.(detail);
|
|
142
|
+
};
|
|
143
|
+
const update = (event) => {
|
|
144
|
+
if (composing || input.matches(':disabled') || input.readOnly)
|
|
145
|
+
return;
|
|
146
|
+
const logical = logicalCount(input.value.slice(0, input.selectionStart ?? input.value.length));
|
|
147
|
+
const parsed = parseLumenAmountDraft(input.value, options, 'inputType' in event && event.inputType === 'insertFromPaste');
|
|
148
|
+
if (parsed === undefined) {
|
|
149
|
+
sync();
|
|
150
|
+
input.setCustomValidity(root.getAttribute('invalid-message') ?? 'Enter a valid amount using the displayed locale and precision.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
draft = parsed;
|
|
154
|
+
sync();
|
|
155
|
+
placeAmountCaret(input, group, logical);
|
|
156
|
+
emitChange();
|
|
157
|
+
};
|
|
158
|
+
const startComposition = () => {
|
|
159
|
+
composing = true;
|
|
160
|
+
};
|
|
161
|
+
const endComposition = (event) => {
|
|
162
|
+
composing = false;
|
|
163
|
+
update(event);
|
|
164
|
+
};
|
|
165
|
+
const reset = (event) => {
|
|
166
|
+
if (event.target !== input.form)
|
|
167
|
+
return;
|
|
168
|
+
clearTimeout(resetTimer);
|
|
169
|
+
resetTimer = setTimeout(() => {
|
|
170
|
+
if (destroyed || event.defaultPrevented || event.target !== input.form)
|
|
171
|
+
return;
|
|
172
|
+
draft = root.getAttribute('default-value') ?? initial;
|
|
173
|
+
sync();
|
|
174
|
+
onChange?.({ draft, value: getLumenAmountValue(draft) });
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
sync();
|
|
178
|
+
input.addEventListener('input', update);
|
|
179
|
+
input.addEventListener('compositionstart', startComposition);
|
|
180
|
+
input.addEventListener('compositionend', endComposition);
|
|
181
|
+
let eventRoot;
|
|
182
|
+
const refresh = () => {
|
|
183
|
+
const current = input.getRootNode();
|
|
184
|
+
if (destroyed || current === eventRoot)
|
|
185
|
+
return;
|
|
186
|
+
clearTimeout(resetTimer);
|
|
187
|
+
eventRoot?.removeEventListener('reset', reset, { capture: true });
|
|
188
|
+
eventRoot = current;
|
|
189
|
+
eventRoot.addEventListener('reset', reset, { capture: true });
|
|
190
|
+
};
|
|
191
|
+
refresh();
|
|
192
|
+
return {
|
|
193
|
+
refresh,
|
|
194
|
+
setValue: value => {
|
|
195
|
+
draft = value;
|
|
196
|
+
sync();
|
|
197
|
+
},
|
|
198
|
+
destroy: () => {
|
|
199
|
+
destroyed = true;
|
|
200
|
+
clearTimeout(resetTimer);
|
|
201
|
+
input.removeEventListener('input', update);
|
|
202
|
+
input.removeEventListener('compositionstart', startComposition);
|
|
203
|
+
input.removeEventListener('compositionend', endComposition);
|
|
204
|
+
eventRoot?.removeEventListener('reset', reset, { capture: true });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type LumenAttachmentPreviewState = 'error' | 'loading' | 'ready' | 'unavailable';
|
|
2
|
+
export interface LumenAttachmentPreviewLabels {
|
|
3
|
+
error: string;
|
|
4
|
+
loading: string;
|
|
5
|
+
unavailable: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const lumenAttachmentPreviewLabels: Readonly<LumenAttachmentPreviewLabels>;
|
|
8
|
+
export declare const resolveLumenAttachmentPreviewState: (src?: unknown, contentType?: unknown, state?: LumenAttachmentPreviewState, failed?: boolean) => LumenAttachmentPreviewState;
|
|
9
|
+
export interface LumenAttachmentPreviewController {
|
|
10
|
+
destroy: () => void;
|
|
11
|
+
refresh: () => void;
|
|
12
|
+
}
|
|
13
|
+
/** Enhances a browser-owned image; never fetches a file or embeds a document. */
|
|
14
|
+
export declare const createLumenAttachmentPreviewController: (root: HTMLElement) => LumenAttachmentPreviewController;
|
|
15
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../src/attachments.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,2BAA2B,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,CAAA;AAEvF,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,4BAA4B,EAAE,QAAQ,CAAC,4BAA4B,CAI/E,CAAA;AAQD,eAAO,MAAM,kCAAkC,GAC7C,MAAM,OAAO,EACb,cAAc,OAAO,EACrB,QAAQ,2BAA2B,EACnC,gBAAc,KACb,2BAMF,CAAA;AAID,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB;AAED,iFAAiF;AACjF,eAAO,MAAM,sCAAsC,GAAI,MAAM,WAAW,KAAG,gCAqI1E,CAAA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const lumenAttachmentPreviewLabels = {
|
|
2
|
+
error: 'Could not load the preview.',
|
|
3
|
+
loading: 'Loading preview…',
|
|
4
|
+
unavailable: 'A preview is not available for this file.'
|
|
5
|
+
};
|
|
6
|
+
const isImageContentType = (contentType) => {
|
|
7
|
+
if (contentType === undefined || contentType === null || contentType === '')
|
|
8
|
+
return true;
|
|
9
|
+
return typeof contentType === 'string' && contentType.trim().toLowerCase().startsWith('image/');
|
|
10
|
+
};
|
|
11
|
+
export const resolveLumenAttachmentPreviewState = (src, contentType, state, failed = false) => {
|
|
12
|
+
if (state === 'loading' || state === 'error' || state === 'unavailable')
|
|
13
|
+
return state;
|
|
14
|
+
if (typeof src !== 'string' || !src.trim() || !isImageContentType(contentType))
|
|
15
|
+
return 'unavailable';
|
|
16
|
+
return failed ? 'error' : 'ready';
|
|
17
|
+
};
|
|
18
|
+
const requestedState = (value) => value === 'error' || value === 'loading' || value === 'ready' || value === 'unavailable' ? value : undefined;
|
|
19
|
+
/** Enhances a browser-owned image; never fetches a file or embeds a document. */
|
|
20
|
+
export const createLumenAttachmentPreviewController = (root) => {
|
|
21
|
+
const owned = (selector) => Array.from(root.querySelectorAll(selector)).find(element => element.closest('[data-ui-attachment-preview]') === root);
|
|
22
|
+
const currentImage = () => {
|
|
23
|
+
const candidate = owned('[data-ui-attachment-preview-image]');
|
|
24
|
+
const isImage = (element) => element?.namespaceURI === 'http://www.w3.org/1999/xhtml' && element.localName === 'img';
|
|
25
|
+
return isImage(candidate) ? candidate : undefined;
|
|
26
|
+
};
|
|
27
|
+
let image = currentImage();
|
|
28
|
+
const abort = new AbortController();
|
|
29
|
+
const cachedFailure = (candidate) => Boolean(candidate?.getAttribute('src') && candidate.complete && candidate.naturalWidth === 0);
|
|
30
|
+
const attribute = (name) => root.getAttribute(name) ?? root.getAttribute(`data-ui-attachment-preview-${name}`);
|
|
31
|
+
let failed = cachedFailure(image);
|
|
32
|
+
let source = image?.getAttribute('src') ?? '';
|
|
33
|
+
let retryKey = attribute('retry-key') ?? '';
|
|
34
|
+
let previous;
|
|
35
|
+
let destroyed = false;
|
|
36
|
+
const reloadImage = () => {
|
|
37
|
+
if (image && source)
|
|
38
|
+
image.setAttribute('src', source);
|
|
39
|
+
};
|
|
40
|
+
const updateSource = () => {
|
|
41
|
+
const nextImage = currentImage();
|
|
42
|
+
if (image !== nextImage) {
|
|
43
|
+
image = nextImage;
|
|
44
|
+
failed = cachedFailure(image);
|
|
45
|
+
}
|
|
46
|
+
const nextSource = image?.getAttribute('src') ?? '';
|
|
47
|
+
const nextRetryKey = attribute('retry-key') ?? '';
|
|
48
|
+
if (source === nextSource && retryKey === nextRetryKey)
|
|
49
|
+
return;
|
|
50
|
+
const retry = retryKey !== nextRetryKey;
|
|
51
|
+
source = nextSource;
|
|
52
|
+
retryKey = nextRetryKey;
|
|
53
|
+
failed = !retry && cachedFailure(image);
|
|
54
|
+
if (retry)
|
|
55
|
+
reloadImage();
|
|
56
|
+
};
|
|
57
|
+
const renderState = (state) => {
|
|
58
|
+
const media = owned('[data-slot="attachment-preview-media"]');
|
|
59
|
+
const message = owned('[data-ui-attachment-preview-message]');
|
|
60
|
+
root.dataset.state = state;
|
|
61
|
+
root.setAttribute('aria-busy', String(state === 'loading'));
|
|
62
|
+
if (media)
|
|
63
|
+
media.hidden = state !== 'ready';
|
|
64
|
+
if (!message)
|
|
65
|
+
return;
|
|
66
|
+
message.hidden = state === 'ready';
|
|
67
|
+
if (state !== 'ready')
|
|
68
|
+
message.textContent = attribute(`${state}-label`) ?? lumenAttachmentPreviewLabels[state];
|
|
69
|
+
};
|
|
70
|
+
const refresh = () => {
|
|
71
|
+
if (destroyed)
|
|
72
|
+
return;
|
|
73
|
+
updateSource();
|
|
74
|
+
const state = resolveLumenAttachmentPreviewState(source, attribute('content-type'), requestedState(attribute('state')), failed);
|
|
75
|
+
renderState(state);
|
|
76
|
+
if (previous !== undefined && previous !== state) {
|
|
77
|
+
const EventConstructor = root.ownerDocument.defaultView?.CustomEvent ?? CustomEvent;
|
|
78
|
+
root.dispatchEvent(new EventConstructor('ui:attachment-preview-change', { bubbles: true, detail: { state } }));
|
|
79
|
+
}
|
|
80
|
+
previous = state;
|
|
81
|
+
};
|
|
82
|
+
root.addEventListener('error', event => {
|
|
83
|
+
if (!root.isConnected || event.target !== currentImage())
|
|
84
|
+
return;
|
|
85
|
+
failed = true;
|
|
86
|
+
refresh();
|
|
87
|
+
}, { capture: true, signal: abort.signal });
|
|
88
|
+
root.addEventListener('load', event => {
|
|
89
|
+
if (!root.isConnected || event.target !== currentImage())
|
|
90
|
+
return;
|
|
91
|
+
failed = false;
|
|
92
|
+
refresh();
|
|
93
|
+
}, { capture: true, signal: abort.signal });
|
|
94
|
+
const observer = new MutationObserver(records => {
|
|
95
|
+
if (records.some(record => record.type === 'attributes' || Array.from(record.addedNodes).some(node => node.nodeType === 1) || Array.from(record.removedNodes).some(node => node.nodeType === 1)))
|
|
96
|
+
refresh();
|
|
97
|
+
});
|
|
98
|
+
observer.observe(root, { subtree: true,
|
|
99
|
+
childList: true,
|
|
100
|
+
attributes: true,
|
|
101
|
+
attributeFilter: [
|
|
102
|
+
'src',
|
|
103
|
+
'state',
|
|
104
|
+
'content-type',
|
|
105
|
+
'retry-key',
|
|
106
|
+
'error-label',
|
|
107
|
+
'loading-label',
|
|
108
|
+
'unavailable-label',
|
|
109
|
+
'data-ui-attachment-preview-state',
|
|
110
|
+
'data-ui-attachment-preview-content-type',
|
|
111
|
+
'data-ui-attachment-preview-retry-key',
|
|
112
|
+
'data-ui-attachment-preview-error-label',
|
|
113
|
+
'data-ui-attachment-preview-loading-label',
|
|
114
|
+
'data-ui-attachment-preview-unavailable-label'
|
|
115
|
+
] });
|
|
116
|
+
refresh();
|
|
117
|
+
return { refresh,
|
|
118
|
+
destroy: () => {
|
|
119
|
+
destroyed = true;
|
|
120
|
+
abort.abort();
|
|
121
|
+
observer.disconnect();
|
|
122
|
+
} };
|
|
123
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type LumenChartDomain, type LumenChartTone } from './charts.js';
|
|
2
|
+
export interface LumenBulletRange {
|
|
3
|
+
end: number;
|
|
4
|
+
label: string;
|
|
5
|
+
tone?: LumenChartTone;
|
|
6
|
+
}
|
|
7
|
+
export interface LumenBulletOptions {
|
|
8
|
+
domain?: LumenChartDomain | undefined;
|
|
9
|
+
ranges?: readonly LumenBulletRange[] | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** A zero-based actual-versus-target comparison with optional qualitative ranges. */
|
|
12
|
+
export declare const createLumenBulletGeometry: (value: number | null, target: number, options?: LumenBulletOptions) => {
|
|
13
|
+
domain: LumenChartDomain;
|
|
14
|
+
ranges: {
|
|
15
|
+
start: number;
|
|
16
|
+
startRatio: number;
|
|
17
|
+
endRatio: number;
|
|
18
|
+
end: number;
|
|
19
|
+
label: string;
|
|
20
|
+
tone?: LumenChartTone;
|
|
21
|
+
}[];
|
|
22
|
+
target: number;
|
|
23
|
+
targetRatio: number;
|
|
24
|
+
ticks: {
|
|
25
|
+
value: number;
|
|
26
|
+
position: number;
|
|
27
|
+
}[];
|
|
28
|
+
valid: boolean;
|
|
29
|
+
value: number | null;
|
|
30
|
+
valueStartRatio: number;
|
|
31
|
+
valueWidthRatio: number;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=bullet-chart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bullet-chart.d.ts","sourceRoot":"","sources":["../src/bullet-chart.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAyC,MAAM,aAAa,CAAA;AAE/G,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,cAAc,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAA;IACrC,MAAM,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAAG,SAAS,CAAA;CACjD;AAoED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,GAAI,OAAO,MAAM,GAAG,IAAI,EAAE,QAAQ,MAAM,EAAE,UAAS,kBAAuB;;;;;;aA7EzG,MAAM;eACJ,MAAM;eACN,cAAc;;;;;;;;;;;;CAyGtB,CAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { lumenChartTones, scaleLumenChartValue } from './charts.js';
|
|
2
|
+
const validBulletTone = (value) => value === undefined || lumenChartTones.some(tone => tone === value);
|
|
3
|
+
const validBulletRange = (range) => {
|
|
4
|
+
if (typeof range !== 'object' || range === null)
|
|
5
|
+
return false;
|
|
6
|
+
if (!('end' in range) || !('label' in range))
|
|
7
|
+
return false;
|
|
8
|
+
return Number.isFinite(range.end) && typeof range.label === 'string' &&
|
|
9
|
+
validBulletTone('tone' in range ? range.tone : undefined);
|
|
10
|
+
};
|
|
11
|
+
const validatedBulletRanges = (input) => {
|
|
12
|
+
if (!Array.isArray(input))
|
|
13
|
+
return null;
|
|
14
|
+
const rows = Array.from(input);
|
|
15
|
+
return rows.every(validBulletRange) ? rows : null;
|
|
16
|
+
};
|
|
17
|
+
const automaticBulletDomain = (values) => {
|
|
18
|
+
let minimum = 0;
|
|
19
|
+
let maximum = 0;
|
|
20
|
+
for (const number of values) {
|
|
21
|
+
if (!Number.isFinite(number))
|
|
22
|
+
continue;
|
|
23
|
+
minimum = Math.min(minimum, number);
|
|
24
|
+
maximum = Math.max(maximum, number);
|
|
25
|
+
}
|
|
26
|
+
return { min: minimum, max: minimum === maximum ? minimum + 1 : maximum };
|
|
27
|
+
};
|
|
28
|
+
const validBulletDomain = (domain) => {
|
|
29
|
+
const finite = Number.isFinite(domain.min) && Number.isFinite(domain.max);
|
|
30
|
+
return finite && domain.min < domain.max && domain.min <= 0 && domain.max >= 0;
|
|
31
|
+
};
|
|
32
|
+
const orderedBulletRanges = (ranges, domain) => {
|
|
33
|
+
let previousEnd = domain.min;
|
|
34
|
+
return ranges.every(range => {
|
|
35
|
+
const valid = range.label.trim().length > 0 && range.end > previousEnd;
|
|
36
|
+
previousEnd = range.end;
|
|
37
|
+
return valid;
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const resolveBulletModel = (value, target, options) => {
|
|
41
|
+
const { ranges: configuredRanges = [] } = options;
|
|
42
|
+
const inputRanges = validatedBulletRanges(configuredRanges);
|
|
43
|
+
const finiteRanges = inputRanges !== null;
|
|
44
|
+
const ranges = [...inputRanges ?? []].sort((left, right) => left.end - right.end);
|
|
45
|
+
const values = [0, target, ...ranges.map(range => range.end), ...value === null ? [] : [value]];
|
|
46
|
+
const requested = options.domain ?? automaticBulletDomain(values);
|
|
47
|
+
const domain = validBulletDomain(requested) ? requested : { min: 0, max: 1 };
|
|
48
|
+
const withinDomain = values.every(number => Number.isFinite(number) && number >= domain.min && number <= domain.max);
|
|
49
|
+
const valid = finiteRanges && validBulletDomain(requested) && withinDomain && orderedBulletRanges(ranges, domain);
|
|
50
|
+
return { domain, ranges, valid };
|
|
51
|
+
};
|
|
52
|
+
/** A zero-based actual-versus-target comparison with optional qualitative ranges. */
|
|
53
|
+
export const createLumenBulletGeometry = (value, target, options = {}) => {
|
|
54
|
+
const { domain, ranges, valid } = resolveBulletModel(value, target, options);
|
|
55
|
+
const position = (number) => scaleLumenChartValue(number, domain, 0, 1);
|
|
56
|
+
const zero = position(0);
|
|
57
|
+
const actual = valid && value !== null ? position(value) : zero;
|
|
58
|
+
let start = domain.min;
|
|
59
|
+
return {
|
|
60
|
+
domain,
|
|
61
|
+
ranges: valid ?
|
|
62
|
+
ranges.map(range => {
|
|
63
|
+
const segment = { ...range, start, startRatio: position(start), endRatio: position(range.end) };
|
|
64
|
+
start = range.end;
|
|
65
|
+
return segment;
|
|
66
|
+
}) :
|
|
67
|
+
[],
|
|
68
|
+
target,
|
|
69
|
+
targetRatio: valid ? position(target) : zero,
|
|
70
|
+
ticks: valid ?
|
|
71
|
+
[domain.min, domain.min / 2 + domain.max / 2, domain.max].map(tick => ({
|
|
72
|
+
value: tick, position: position(tick)
|
|
73
|
+
})) :
|
|
74
|
+
[],
|
|
75
|
+
valid,
|
|
76
|
+
value,
|
|
77
|
+
valueStartRatio: Math.min(zero, actual),
|
|
78
|
+
valueWidthRatio: Math.abs(actual - zero)
|
|
79
|
+
};
|
|
80
|
+
};
|