@neptune.fintech/web-ui 2.1.0 → 2.3.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/dist/components/cards.d.ts +64 -0
- package/dist/components/cards.d.ts.map +1 -0
- package/dist/components/cards.js +508 -0
- package/dist/components/cards.js.map +1 -0
- package/dist/components/corporate.d.ts +84 -0
- package/dist/components/corporate.d.ts.map +1 -0
- package/dist/components/corporate.js +782 -0
- package/dist/components/corporate.js.map +1 -0
- package/dist/components/data-viz.d.ts +69 -0
- package/dist/components/data-viz.d.ts.map +1 -0
- package/dist/components/data-viz.js +526 -0
- package/dist/components/data-viz.js.map +1 -0
- package/dist/components/feedback-status.d.ts +80 -0
- package/dist/components/feedback-status.d.ts.map +1 -0
- package/dist/components/feedback-status.js +537 -0
- package/dist/components/feedback-status.js.map +1 -0
- package/dist/components/money-inputs.d.ts +105 -0
- package/dist/components/money-inputs.d.ts.map +1 -0
- package/dist/components/money-inputs.js +766 -0
- package/dist/components/money-inputs.js.map +1 -0
- package/dist/components/money-movement.d.ts +79 -0
- package/dist/components/money-movement.d.ts.map +1 -0
- package/dist/components/money-movement.js +740 -0
- package/dist/components/money-movement.js.map +1 -0
- package/dist/components/premium.d.ts +38 -0
- package/dist/components/premium.d.ts.map +1 -0
- package/dist/components/premium.js +275 -0
- package/dist/components/premium.js.map +1 -0
- package/dist/components/shell-layout.d.ts +103 -0
- package/dist/components/shell-layout.d.ts.map +1 -0
- package/dist/components/shell-layout.js +582 -0
- package/dist/components/shell-layout.js.map +1 -0
- package/dist/components/wallet-pay.d.ts +85 -0
- package/dist/components/wallet-pay.d.ts.map +1 -0
- package/dist/components/wallet-pay.js +633 -0
- package/dist/components/wallet-pay.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/register.d.ts.map +1 -1
- package/dist/register.js +70 -0
- package/dist/register.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
// © 2026 Neptune.Fintech (neptune.ly) · Neptune Odyssey Community License v1.0
|
|
2
|
+
// Neptune Odyssey — money & secure-entry inputs
|
|
3
|
+
// <npt-amount-input>, <npt-currency-field>, <npt-iban-field>,
|
|
4
|
+
// <npt-otp-input>, <npt-pin-input>, <npt-amount-keypad>.
|
|
5
|
+
// Custom-property driven only; money uses tabular figures; logical layout → mirrors in RTL.
|
|
6
|
+
import { NptElement, css, html, A11Y } from "./base.js";
|
|
7
|
+
const esc = (v) => (v ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
8
|
+
/** Keep only digits and a single decimal point (first one wins). */
|
|
9
|
+
const sanitizeAmount = (raw) => {
|
|
10
|
+
let seenDot = false;
|
|
11
|
+
let out = "";
|
|
12
|
+
for (const ch of raw) {
|
|
13
|
+
if (ch >= "0" && ch <= "9") {
|
|
14
|
+
out += ch;
|
|
15
|
+
}
|
|
16
|
+
else if ((ch === "." || ch === ",") && !seenDot) {
|
|
17
|
+
seenDot = true;
|
|
18
|
+
out += ".";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
/** Strip everything but A–Z and 0–9, uppercased — for IBANs. */
|
|
24
|
+
const cleanIban = (raw) => raw.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
25
|
+
/** Space-group an IBAN into blocks of four. */
|
|
26
|
+
const groupIban = (raw) => cleanIban(raw).replace(/(.{4})/g, "$1 ").trim();
|
|
27
|
+
/**
|
|
28
|
+
* ISO 7064 mod-97-10 validation of an IBAN. Returns true for a structurally
|
|
29
|
+
* valid checksum (length 15–34, known country prefix length not enforced here).
|
|
30
|
+
*/
|
|
31
|
+
const isValidIban = (raw) => {
|
|
32
|
+
const v = cleanIban(raw);
|
|
33
|
+
if (v.length < 15 || v.length > 34)
|
|
34
|
+
return false;
|
|
35
|
+
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(v))
|
|
36
|
+
return false;
|
|
37
|
+
const rearranged = v.slice(4) + v.slice(0, 4);
|
|
38
|
+
let remainder = 0;
|
|
39
|
+
for (const ch of rearranged) {
|
|
40
|
+
const code = ch >= "A" && ch <= "Z" ? (ch.charCodeAt(0) - 55).toString() : ch;
|
|
41
|
+
for (const d of code) {
|
|
42
|
+
remainder = (remainder * 10 + (d.charCodeAt(0) - 48)) % 97;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return remainder === 1;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* <npt-amount-input currency="LYD" value="" placeholder="0.00"></npt-amount-input>
|
|
49
|
+
* Large amount entry with a currency affix. Big tabular figures. Numeric input
|
|
50
|
+
* mode; dispatches `input` (bubbling) with the sanitized value on every change.
|
|
51
|
+
*/
|
|
52
|
+
export class NptAmountInput extends NptElement {
|
|
53
|
+
constructor() {
|
|
54
|
+
super(...arguments);
|
|
55
|
+
this.onInput = (e) => {
|
|
56
|
+
const input = e.target;
|
|
57
|
+
const clean = sanitizeAmount(input.value);
|
|
58
|
+
if (input.value !== clean)
|
|
59
|
+
input.value = clean;
|
|
60
|
+
// Reflect without re-rendering (keeps caret stable).
|
|
61
|
+
this.setAttribute("value", clean);
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { value: clean } }));
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
attributeChangedCallback() {
|
|
67
|
+
if (this.isConnected)
|
|
68
|
+
this.update();
|
|
69
|
+
}
|
|
70
|
+
get value() {
|
|
71
|
+
return this.root.querySelector("input")?.value ?? this.getAttribute("value") ?? "";
|
|
72
|
+
}
|
|
73
|
+
set value(v) {
|
|
74
|
+
this.setAttribute("value", v);
|
|
75
|
+
}
|
|
76
|
+
connectedCallback() {
|
|
77
|
+
super.connectedCallback();
|
|
78
|
+
this.root.addEventListener("input", this.onInput);
|
|
79
|
+
}
|
|
80
|
+
disconnectedCallback() {
|
|
81
|
+
this.root.removeEventListener("input", this.onInput);
|
|
82
|
+
}
|
|
83
|
+
styles() {
|
|
84
|
+
return css `
|
|
85
|
+
${A11Y}
|
|
86
|
+
:host {
|
|
87
|
+
display: block;
|
|
88
|
+
}
|
|
89
|
+
.field {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: baseline;
|
|
92
|
+
gap: var(--npt-space-2, 8px);
|
|
93
|
+
min-height: 64px;
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
padding-inline: var(--npt-space-4, 16px);
|
|
96
|
+
padding-block: var(--npt-space-3, 12px);
|
|
97
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
98
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
99
|
+
border-radius: var(--npt-corner-md, 16px);
|
|
100
|
+
transition: border-color var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease),
|
|
101
|
+
box-shadow var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease);
|
|
102
|
+
}
|
|
103
|
+
.field:focus-within {
|
|
104
|
+
border-color: var(--md-sys-color-primary);
|
|
105
|
+
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
|
106
|
+
}
|
|
107
|
+
:host([disabled]) .field {
|
|
108
|
+
opacity: 0.38;
|
|
109
|
+
}
|
|
110
|
+
.affix {
|
|
111
|
+
font-family: var(--npt-font-text);
|
|
112
|
+
font-size: var(--npt-text-title, 18px);
|
|
113
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
114
|
+
flex: 0 0 auto;
|
|
115
|
+
}
|
|
116
|
+
input {
|
|
117
|
+
flex: 1 1 auto;
|
|
118
|
+
min-inline-size: 0;
|
|
119
|
+
inline-size: 100%;
|
|
120
|
+
border: none;
|
|
121
|
+
outline: none;
|
|
122
|
+
background: transparent;
|
|
123
|
+
color: var(--md-sys-color-on-surface);
|
|
124
|
+
font-family: var(--npt-font-num);
|
|
125
|
+
font-variant-numeric: tabular-nums;
|
|
126
|
+
font-feature-settings: "tnum" 1;
|
|
127
|
+
font-size: var(--npt-text-display-md, 45px);
|
|
128
|
+
line-height: var(--npt-leading-display-md, 52px);
|
|
129
|
+
font-weight: var(--npt-display-weight, 700);
|
|
130
|
+
letter-spacing: var(--npt-display-tracking, -0.02em);
|
|
131
|
+
text-align: end;
|
|
132
|
+
}
|
|
133
|
+
input::placeholder {
|
|
134
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
135
|
+
opacity: 0.5;
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
render() {
|
|
140
|
+
const currency = esc(this.getAttribute("currency"));
|
|
141
|
+
const suffix = esc(this.getAttribute("suffix"));
|
|
142
|
+
const value = esc(this.getAttribute("value"));
|
|
143
|
+
const placeholder = esc(this.getAttribute("placeholder")) || "0.00";
|
|
144
|
+
const disabled = this.hasAttribute("disabled") ? "disabled" : "";
|
|
145
|
+
return html `<div class="field" part="field">
|
|
146
|
+
${currency ? html `<span class="affix" part="prefix" aria-hidden="true">${currency}</span>` : ""}
|
|
147
|
+
<input
|
|
148
|
+
type="text"
|
|
149
|
+
inputmode="decimal"
|
|
150
|
+
autocomplete="off"
|
|
151
|
+
value="${value}"
|
|
152
|
+
placeholder="${placeholder}"
|
|
153
|
+
aria-label="Amount${currency ? html ` in ${currency}` : ""}"
|
|
154
|
+
${disabled}
|
|
155
|
+
/>
|
|
156
|
+
${suffix ? html `<span class="affix" part="suffix" aria-hidden="true">${suffix}</span>` : ""}
|
|
157
|
+
</div>`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
NptAmountInput.observedAttributes = ["currency", "value", "placeholder", "suffix", "disabled"];
|
|
161
|
+
/**
|
|
162
|
+
* <npt-currency-field label="Amount" value="" currency="LYD"
|
|
163
|
+
* helper="Available 12,480.50" [error="msg"]></npt-currency-field>
|
|
164
|
+
* Labelled outlined money field. Tabular figures; trailing currency code.
|
|
165
|
+
*/
|
|
166
|
+
export class NptCurrencyField extends NptElement {
|
|
167
|
+
constructor() {
|
|
168
|
+
super(...arguments);
|
|
169
|
+
this.onInput = (e) => {
|
|
170
|
+
const input = e.target;
|
|
171
|
+
const clean = sanitizeAmount(input.value);
|
|
172
|
+
if (input.value !== clean)
|
|
173
|
+
input.value = clean;
|
|
174
|
+
this.setAttribute("value", clean);
|
|
175
|
+
e.stopPropagation();
|
|
176
|
+
this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { value: clean } }));
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
attributeChangedCallback() {
|
|
180
|
+
if (this.isConnected)
|
|
181
|
+
this.update();
|
|
182
|
+
}
|
|
183
|
+
get value() {
|
|
184
|
+
return this.root.querySelector("input")?.value ?? this.getAttribute("value") ?? "";
|
|
185
|
+
}
|
|
186
|
+
connectedCallback() {
|
|
187
|
+
super.connectedCallback();
|
|
188
|
+
this.root.addEventListener("input", this.onInput);
|
|
189
|
+
}
|
|
190
|
+
disconnectedCallback() {
|
|
191
|
+
this.root.removeEventListener("input", this.onInput);
|
|
192
|
+
}
|
|
193
|
+
styles() {
|
|
194
|
+
return css `
|
|
195
|
+
${A11Y}
|
|
196
|
+
:host {
|
|
197
|
+
display: block;
|
|
198
|
+
}
|
|
199
|
+
label {
|
|
200
|
+
display: block;
|
|
201
|
+
font-family: var(--npt-font-text);
|
|
202
|
+
font-size: var(--npt-text-label, 14px);
|
|
203
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
204
|
+
margin-block-end: var(--npt-space-2, 8px);
|
|
205
|
+
}
|
|
206
|
+
.field {
|
|
207
|
+
display: flex;
|
|
208
|
+
align-items: center;
|
|
209
|
+
gap: var(--npt-space-3, 12px);
|
|
210
|
+
min-height: 48px;
|
|
211
|
+
box-sizing: border-box;
|
|
212
|
+
padding-inline: var(--npt-space-4, 16px);
|
|
213
|
+
padding-block: var(--npt-space-3, 12px);
|
|
214
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
215
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
216
|
+
border-radius: var(--npt-corner-sm, 12px);
|
|
217
|
+
transition: border-color var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease),
|
|
218
|
+
box-shadow var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease);
|
|
219
|
+
}
|
|
220
|
+
.field:focus-within {
|
|
221
|
+
border-color: var(--md-sys-color-primary);
|
|
222
|
+
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
|
223
|
+
}
|
|
224
|
+
:host([error]) .field {
|
|
225
|
+
border-color: var(--md-sys-color-error);
|
|
226
|
+
}
|
|
227
|
+
:host([disabled]) .field {
|
|
228
|
+
opacity: 0.38;
|
|
229
|
+
}
|
|
230
|
+
input {
|
|
231
|
+
flex: 1 1 auto;
|
|
232
|
+
min-inline-size: 0;
|
|
233
|
+
border: none;
|
|
234
|
+
outline: none;
|
|
235
|
+
background: transparent;
|
|
236
|
+
color: var(--md-sys-color-on-surface);
|
|
237
|
+
font-family: var(--npt-font-num);
|
|
238
|
+
font-variant-numeric: tabular-nums;
|
|
239
|
+
font-feature-settings: "tnum" 1;
|
|
240
|
+
font-size: var(--npt-text-title, 18px);
|
|
241
|
+
text-align: end;
|
|
242
|
+
}
|
|
243
|
+
input::placeholder {
|
|
244
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
245
|
+
opacity: 0.6;
|
|
246
|
+
}
|
|
247
|
+
.currency {
|
|
248
|
+
flex: 0 0 auto;
|
|
249
|
+
font-family: var(--npt-font-text);
|
|
250
|
+
font-size: var(--npt-text-body, 14px);
|
|
251
|
+
font-weight: 600;
|
|
252
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
253
|
+
}
|
|
254
|
+
.helper {
|
|
255
|
+
font-family: var(--npt-font-text);
|
|
256
|
+
font-size: var(--npt-text-caption, 12px);
|
|
257
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
258
|
+
margin-block-start: var(--npt-space-2, 8px);
|
|
259
|
+
}
|
|
260
|
+
:host([error]) .helper {
|
|
261
|
+
color: var(--md-sys-color-error);
|
|
262
|
+
}
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
render() {
|
|
266
|
+
const label = esc(this.getAttribute("label"));
|
|
267
|
+
const value = esc(this.getAttribute("value"));
|
|
268
|
+
const currency = esc(this.getAttribute("currency"));
|
|
269
|
+
const helper = esc(this.getAttribute("helper"));
|
|
270
|
+
const error = this.getAttribute("error");
|
|
271
|
+
const placeholder = esc(this.getAttribute("placeholder")) || "0.00";
|
|
272
|
+
const disabled = this.hasAttribute("disabled") ? "disabled" : "";
|
|
273
|
+
const helperText = error ? esc(error) : helper;
|
|
274
|
+
return html `
|
|
275
|
+
${label ? html `<label id="lbl">${label}</label>` : ""}
|
|
276
|
+
<div class="field" part="field">
|
|
277
|
+
<input
|
|
278
|
+
type="text"
|
|
279
|
+
inputmode="decimal"
|
|
280
|
+
autocomplete="off"
|
|
281
|
+
value="${value}"
|
|
282
|
+
placeholder="${placeholder}"
|
|
283
|
+
aria-labelledby="${label ? "lbl" : ""}"
|
|
284
|
+
aria-label="${label || "Amount"}"
|
|
285
|
+
aria-invalid="${error ? "true" : "false"}"
|
|
286
|
+
${disabled}
|
|
287
|
+
/>
|
|
288
|
+
${currency ? html `<span class="currency" part="currency">${currency}</span>` : ""}
|
|
289
|
+
</div>
|
|
290
|
+
${helperText
|
|
291
|
+
? html `<p class="helper" part="helper" role="${error ? "alert" : "note"}">${helperText}</p>`
|
|
292
|
+
: ""}
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
NptCurrencyField.observedAttributes = ["label", "value", "currency", "helper", "placeholder", "error", "disabled"];
|
|
297
|
+
/**
|
|
298
|
+
* <npt-iban-field label="IBAN" value="" country="LY"></npt-iban-field>
|
|
299
|
+
* Formats the IBAN into groups of four as you type and reflects valid/invalid
|
|
300
|
+
* state (ISO 7064 mod-97). Dispatches `input` and `change` with `{ value, valid }`.
|
|
301
|
+
*/
|
|
302
|
+
export class NptIbanField extends NptElement {
|
|
303
|
+
constructor() {
|
|
304
|
+
super(...arguments);
|
|
305
|
+
this.onInput = (e) => {
|
|
306
|
+
const input = e.target;
|
|
307
|
+
const raw = cleanIban(input.value);
|
|
308
|
+
const formatted = groupIban(raw);
|
|
309
|
+
input.value = formatted;
|
|
310
|
+
// Keep the caret at the end (typical for grouped entry).
|
|
311
|
+
const end = formatted.length;
|
|
312
|
+
input.setSelectionRange(end, end);
|
|
313
|
+
this.setAttribute("value", raw);
|
|
314
|
+
this.setState();
|
|
315
|
+
e.stopPropagation();
|
|
316
|
+
this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { value: raw, valid: isValidIban(raw) } }));
|
|
317
|
+
};
|
|
318
|
+
this.onBlur = () => {
|
|
319
|
+
const raw = this.value;
|
|
320
|
+
this.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: { value: raw, valid: isValidIban(raw) } }));
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
attributeChangedCallback() {
|
|
324
|
+
if (this.isConnected)
|
|
325
|
+
this.update();
|
|
326
|
+
}
|
|
327
|
+
/** Raw IBAN with no spaces. */
|
|
328
|
+
get value() {
|
|
329
|
+
return cleanIban(this.getAttribute("value") ?? "");
|
|
330
|
+
}
|
|
331
|
+
get valid() {
|
|
332
|
+
return isValidIban(this.value);
|
|
333
|
+
}
|
|
334
|
+
connectedCallback() {
|
|
335
|
+
super.connectedCallback();
|
|
336
|
+
this.root.addEventListener("input", this.onInput);
|
|
337
|
+
this.root.addEventListener("blur", this.onBlur, true);
|
|
338
|
+
}
|
|
339
|
+
disconnectedCallback() {
|
|
340
|
+
this.root.removeEventListener("input", this.onInput);
|
|
341
|
+
this.root.removeEventListener("blur", this.onBlur, true);
|
|
342
|
+
}
|
|
343
|
+
setState() {
|
|
344
|
+
const raw = this.value;
|
|
345
|
+
const valid = raw.length >= 15 && isValidIban(raw);
|
|
346
|
+
const invalid = raw.length >= 15 && !valid;
|
|
347
|
+
this.toggleAttribute("data-valid", valid);
|
|
348
|
+
this.toggleAttribute("data-invalid", invalid);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
update() {
|
|
352
|
+
super.update();
|
|
353
|
+
this.setState();
|
|
354
|
+
}
|
|
355
|
+
styles() {
|
|
356
|
+
return css `
|
|
357
|
+
${A11Y}
|
|
358
|
+
:host {
|
|
359
|
+
display: block;
|
|
360
|
+
}
|
|
361
|
+
label {
|
|
362
|
+
display: block;
|
|
363
|
+
font-family: var(--npt-font-text);
|
|
364
|
+
font-size: var(--npt-text-label, 14px);
|
|
365
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
366
|
+
margin-block-end: var(--npt-space-2, 8px);
|
|
367
|
+
}
|
|
368
|
+
.field {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
gap: var(--npt-space-3, 12px);
|
|
372
|
+
min-height: 48px;
|
|
373
|
+
box-sizing: border-box;
|
|
374
|
+
padding-inline: var(--npt-space-4, 16px);
|
|
375
|
+
padding-block: var(--npt-space-3, 12px);
|
|
376
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
377
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
378
|
+
border-radius: var(--npt-corner-sm, 12px);
|
|
379
|
+
transition: border-color var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease),
|
|
380
|
+
box-shadow var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease);
|
|
381
|
+
}
|
|
382
|
+
.field:focus-within {
|
|
383
|
+
border-color: var(--md-sys-color-primary);
|
|
384
|
+
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
|
385
|
+
}
|
|
386
|
+
:host([data-valid]) .field {
|
|
387
|
+
border-color: var(--md-sys-color-success);
|
|
388
|
+
}
|
|
389
|
+
:host([data-invalid]) .field {
|
|
390
|
+
border-color: var(--md-sys-color-error);
|
|
391
|
+
}
|
|
392
|
+
:host([disabled]) .field {
|
|
393
|
+
opacity: 0.38;
|
|
394
|
+
}
|
|
395
|
+
input {
|
|
396
|
+
flex: 1 1 auto;
|
|
397
|
+
min-inline-size: 0;
|
|
398
|
+
border: none;
|
|
399
|
+
outline: none;
|
|
400
|
+
background: transparent;
|
|
401
|
+
color: var(--md-sys-color-on-surface);
|
|
402
|
+
font-family: var(--npt-font-num);
|
|
403
|
+
font-variant-numeric: tabular-nums;
|
|
404
|
+
font-size: var(--npt-text-body-lg, 16px);
|
|
405
|
+
letter-spacing: 0.06em;
|
|
406
|
+
text-transform: uppercase;
|
|
407
|
+
}
|
|
408
|
+
input::placeholder {
|
|
409
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
410
|
+
opacity: 0.6;
|
|
411
|
+
letter-spacing: normal;
|
|
412
|
+
}
|
|
413
|
+
.state {
|
|
414
|
+
flex: 0 0 auto;
|
|
415
|
+
font-size: var(--npt-text-body-lg, 16px);
|
|
416
|
+
line-height: 1;
|
|
417
|
+
display: none;
|
|
418
|
+
}
|
|
419
|
+
:host([data-valid]) .state.ok {
|
|
420
|
+
display: inline;
|
|
421
|
+
color: var(--md-sys-color-success);
|
|
422
|
+
}
|
|
423
|
+
:host([data-invalid]) .state.bad {
|
|
424
|
+
display: inline;
|
|
425
|
+
color: var(--md-sys-color-error);
|
|
426
|
+
}
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
render() {
|
|
430
|
+
const label = esc(this.getAttribute("label"));
|
|
431
|
+
const country = cleanIban(this.getAttribute("country") ?? "");
|
|
432
|
+
const value = groupIban(this.getAttribute("value") ?? "");
|
|
433
|
+
const placeholder = esc(this.getAttribute("placeholder")) || `${country || "LY"}00 0000 0000 0000`;
|
|
434
|
+
const disabled = this.hasAttribute("disabled") ? "disabled" : "";
|
|
435
|
+
return html `
|
|
436
|
+
${label ? html `<label id="lbl">${label}</label>` : ""}
|
|
437
|
+
<div class="field" part="field">
|
|
438
|
+
<input
|
|
439
|
+
type="text"
|
|
440
|
+
inputmode="text"
|
|
441
|
+
autocomplete="off"
|
|
442
|
+
autocapitalize="characters"
|
|
443
|
+
spellcheck="false"
|
|
444
|
+
value="${esc(value)}"
|
|
445
|
+
placeholder="${placeholder}"
|
|
446
|
+
aria-labelledby="${label ? "lbl" : ""}"
|
|
447
|
+
aria-label="${label || "IBAN"}"
|
|
448
|
+
aria-invalid="${this.hasAttribute("data-invalid") ? "true" : "false"}"
|
|
449
|
+
${disabled}
|
|
450
|
+
/>
|
|
451
|
+
<span class="state ok" part="valid-icon" aria-label="Valid" role="img">✓</span>
|
|
452
|
+
<span class="state bad" part="invalid-icon" aria-label="Invalid" role="img">✕</span>
|
|
453
|
+
</div>
|
|
454
|
+
`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
NptIbanField.observedAttributes = ["label", "value", "country", "placeholder", "disabled"];
|
|
458
|
+
/**
|
|
459
|
+
* <npt-otp-input length="6" value="" [masked]></npt-otp-input>
|
|
460
|
+
* N separate single-character boxes with auto-advance, backspace-rewind, and
|
|
461
|
+
* paste-fill. Dispatches `input` on every change and `complete` (with
|
|
462
|
+
* `{ value }`) once every box is filled.
|
|
463
|
+
*/
|
|
464
|
+
export class NptOtpInput extends NptElement {
|
|
465
|
+
constructor() {
|
|
466
|
+
super(...arguments);
|
|
467
|
+
this.onFocus = (e) => {
|
|
468
|
+
e.target.select?.();
|
|
469
|
+
};
|
|
470
|
+
this.onInput = (e) => {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
const target = e.target;
|
|
473
|
+
const digits = target.value.replace(/\D/g, "");
|
|
474
|
+
target.value = digits.slice(-1);
|
|
475
|
+
const boxes = this.boxes();
|
|
476
|
+
const idx = boxes.indexOf(target);
|
|
477
|
+
if (target.value && idx >= 0 && idx < boxes.length - 1) {
|
|
478
|
+
boxes[idx + 1]?.focus();
|
|
479
|
+
}
|
|
480
|
+
this.emit();
|
|
481
|
+
};
|
|
482
|
+
this.onKey = (e) => {
|
|
483
|
+
if (!(e instanceof KeyboardEvent))
|
|
484
|
+
return;
|
|
485
|
+
const target = e.target;
|
|
486
|
+
const boxes = this.boxes();
|
|
487
|
+
const idx = boxes.indexOf(target);
|
|
488
|
+
if (idx < 0)
|
|
489
|
+
return;
|
|
490
|
+
if (e.key === "Backspace" && !target.value && idx > 0) {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
const prev = boxes[idx - 1];
|
|
493
|
+
if (prev) {
|
|
494
|
+
prev.value = "";
|
|
495
|
+
prev.focus();
|
|
496
|
+
this.emit();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else if (e.key === "ArrowLeft" && idx > 0) {
|
|
500
|
+
e.preventDefault();
|
|
501
|
+
boxes[idx - 1]?.focus();
|
|
502
|
+
}
|
|
503
|
+
else if (e.key === "ArrowRight" && idx < boxes.length - 1) {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
boxes[idx + 1]?.focus();
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
this.onPaste = (e) => {
|
|
509
|
+
if (!(e instanceof ClipboardEvent))
|
|
510
|
+
return;
|
|
511
|
+
e.preventDefault();
|
|
512
|
+
const text = (e.clipboardData?.getData("text") ?? "").replace(/\D/g, "");
|
|
513
|
+
if (!text)
|
|
514
|
+
return;
|
|
515
|
+
const boxes = this.boxes();
|
|
516
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
517
|
+
const box = boxes[i];
|
|
518
|
+
if (box)
|
|
519
|
+
box.value = text[i] ?? "";
|
|
520
|
+
}
|
|
521
|
+
const nextEmpty = boxes.find((b) => !b.value) ?? boxes[boxes.length - 1];
|
|
522
|
+
nextEmpty?.focus();
|
|
523
|
+
this.emit();
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
attributeChangedCallback() {
|
|
527
|
+
if (this.isConnected)
|
|
528
|
+
this.update();
|
|
529
|
+
}
|
|
530
|
+
get inputType() {
|
|
531
|
+
return "tel";
|
|
532
|
+
}
|
|
533
|
+
get isMasked() {
|
|
534
|
+
return this.hasAttribute("masked");
|
|
535
|
+
}
|
|
536
|
+
get count() {
|
|
537
|
+
const n = Number(this.getAttribute("length"));
|
|
538
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : this.defaultLength;
|
|
539
|
+
}
|
|
540
|
+
get defaultLength() {
|
|
541
|
+
return 6;
|
|
542
|
+
}
|
|
543
|
+
get value() {
|
|
544
|
+
const boxes = this.root.querySelectorAll("input");
|
|
545
|
+
if (boxes.length)
|
|
546
|
+
return Array.from(boxes, (b) => b.value).join("");
|
|
547
|
+
return (this.getAttribute("value") ?? "").replace(/\D/g, "").slice(0, this.count);
|
|
548
|
+
}
|
|
549
|
+
set value(v) {
|
|
550
|
+
this.setAttribute("value", v);
|
|
551
|
+
}
|
|
552
|
+
connectedCallback() {
|
|
553
|
+
super.connectedCallback();
|
|
554
|
+
this.root.addEventListener("input", this.onInput);
|
|
555
|
+
this.root.addEventListener("keydown", this.onKey);
|
|
556
|
+
this.root.addEventListener("paste", this.onPaste);
|
|
557
|
+
this.root.addEventListener("focus", this.onFocus, true);
|
|
558
|
+
}
|
|
559
|
+
disconnectedCallback() {
|
|
560
|
+
this.root.removeEventListener("input", this.onInput);
|
|
561
|
+
this.root.removeEventListener("keydown", this.onKey);
|
|
562
|
+
this.root.removeEventListener("paste", this.onPaste);
|
|
563
|
+
this.root.removeEventListener("focus", this.onFocus, true);
|
|
564
|
+
}
|
|
565
|
+
boxes() {
|
|
566
|
+
return Array.from(this.root.querySelectorAll("input"));
|
|
567
|
+
}
|
|
568
|
+
emit() {
|
|
569
|
+
const value = this.value;
|
|
570
|
+
this.dispatchEvent(new CustomEvent("input", { bubbles: true, detail: { value } }));
|
|
571
|
+
if (value.length === this.count) {
|
|
572
|
+
this.dispatchEvent(new CustomEvent("complete", { bubbles: true, detail: { value } }));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
styles() {
|
|
576
|
+
return css `
|
|
577
|
+
${A11Y}
|
|
578
|
+
:host {
|
|
579
|
+
display: block;
|
|
580
|
+
}
|
|
581
|
+
.group {
|
|
582
|
+
display: flex;
|
|
583
|
+
gap: var(--npt-space-2, 8px);
|
|
584
|
+
direction: ltr;
|
|
585
|
+
}
|
|
586
|
+
input {
|
|
587
|
+
inline-size: 48px;
|
|
588
|
+
block-size: 56px;
|
|
589
|
+
min-inline-size: 44px;
|
|
590
|
+
box-sizing: border-box;
|
|
591
|
+
text-align: center;
|
|
592
|
+
border: 1px solid var(--md-sys-color-outline);
|
|
593
|
+
border-radius: var(--npt-corner-sm, 12px);
|
|
594
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
595
|
+
color: var(--md-sys-color-on-surface);
|
|
596
|
+
font-family: var(--npt-font-num);
|
|
597
|
+
font-variant-numeric: tabular-nums;
|
|
598
|
+
font-size: var(--npt-text-headline, 24px);
|
|
599
|
+
outline: none;
|
|
600
|
+
transition: border-color var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease),
|
|
601
|
+
box-shadow var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease);
|
|
602
|
+
}
|
|
603
|
+
input:focus {
|
|
604
|
+
border-color: var(--md-sys-color-primary);
|
|
605
|
+
box-shadow: 0 0 0 1px var(--md-sys-color-primary);
|
|
606
|
+
}
|
|
607
|
+
:host([disabled]) input {
|
|
608
|
+
opacity: 0.38;
|
|
609
|
+
}
|
|
610
|
+
`;
|
|
611
|
+
}
|
|
612
|
+
render() {
|
|
613
|
+
const count = this.count;
|
|
614
|
+
const masked = this.isMasked;
|
|
615
|
+
const type = masked ? "password" : this.inputType;
|
|
616
|
+
const value = (this.getAttribute("value") ?? "").replace(/\D/g, "").slice(0, count);
|
|
617
|
+
const disabled = this.hasAttribute("disabled") ? "disabled" : "";
|
|
618
|
+
let boxes = "";
|
|
619
|
+
for (let i = 0; i < count; i++) {
|
|
620
|
+
boxes += html `<input
|
|
621
|
+
type="${type}"
|
|
622
|
+
inputmode="numeric"
|
|
623
|
+
autocomplete="${i === 0 ? "one-time-code" : "off"}"
|
|
624
|
+
maxlength="1"
|
|
625
|
+
value="${esc(value[i] ?? "")}"
|
|
626
|
+
aria-label="Digit ${String(i + 1)} of ${String(count)}"
|
|
627
|
+
${disabled}
|
|
628
|
+
/>`;
|
|
629
|
+
}
|
|
630
|
+
return html `<div class="group" part="group" role="group" aria-label="Verification code">${boxes}</div>`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
NptOtpInput.observedAttributes = ["length", "value", "masked", "disabled"];
|
|
634
|
+
/**
|
|
635
|
+
* <npt-pin-input length="4"></npt-pin-input>
|
|
636
|
+
* Like the OTP input, but always masked with dots. Defaults to 4 boxes.
|
|
637
|
+
*/
|
|
638
|
+
export class NptPinInput extends NptOtpInput {
|
|
639
|
+
get defaultLength() {
|
|
640
|
+
return 4;
|
|
641
|
+
}
|
|
642
|
+
get isMasked() {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
NptPinInput.observedAttributes = ["length", "value", "disabled"];
|
|
647
|
+
/**
|
|
648
|
+
* <npt-amount-keypad value=""></npt-amount-keypad>
|
|
649
|
+
* Numeric keypad (0–9, ., backspace). Dispatches `key` (with the pressed key)
|
|
650
|
+
* on each press and `value` (with the running string) after applying it.
|
|
651
|
+
*/
|
|
652
|
+
export class NptAmountKeypad extends NptElement {
|
|
653
|
+
constructor() {
|
|
654
|
+
super(...arguments);
|
|
655
|
+
this.onClick = (e) => {
|
|
656
|
+
if (this.hasAttribute("disabled"))
|
|
657
|
+
return;
|
|
658
|
+
const btn = e.target?.closest("button[data-key]");
|
|
659
|
+
if (!btn)
|
|
660
|
+
return;
|
|
661
|
+
const key = btn.dataset["key"];
|
|
662
|
+
if (!key)
|
|
663
|
+
return;
|
|
664
|
+
this.dispatchEvent(new CustomEvent("key", { bubbles: true, detail: { key } }));
|
|
665
|
+
const next = this.apply(key);
|
|
666
|
+
this.setAttribute("value", next);
|
|
667
|
+
this.dispatchEvent(new CustomEvent("value", { bubbles: true, detail: { value: next } }));
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
attributeChangedCallback() {
|
|
671
|
+
if (this.isConnected)
|
|
672
|
+
this.update();
|
|
673
|
+
}
|
|
674
|
+
get value() {
|
|
675
|
+
return this.getAttribute("value") ?? "";
|
|
676
|
+
}
|
|
677
|
+
set value(v) {
|
|
678
|
+
this.setAttribute("value", v);
|
|
679
|
+
}
|
|
680
|
+
connectedCallback() {
|
|
681
|
+
super.connectedCallback();
|
|
682
|
+
this.root.addEventListener("click", this.onClick);
|
|
683
|
+
}
|
|
684
|
+
disconnectedCallback() {
|
|
685
|
+
this.root.removeEventListener("click", this.onClick);
|
|
686
|
+
}
|
|
687
|
+
apply(key) {
|
|
688
|
+
const current = this.value;
|
|
689
|
+
if (key === "back")
|
|
690
|
+
return current.slice(0, -1);
|
|
691
|
+
if (key === ".")
|
|
692
|
+
return current.includes(".") ? current : current === "" ? "0." : current + ".";
|
|
693
|
+
return current + key;
|
|
694
|
+
}
|
|
695
|
+
styles() {
|
|
696
|
+
return css `
|
|
697
|
+
${A11Y}
|
|
698
|
+
:host {
|
|
699
|
+
display: block;
|
|
700
|
+
}
|
|
701
|
+
.pad {
|
|
702
|
+
display: grid;
|
|
703
|
+
grid-template-columns: repeat(3, 1fr);
|
|
704
|
+
gap: var(--npt-space-2, 8px);
|
|
705
|
+
}
|
|
706
|
+
button {
|
|
707
|
+
min-height: 56px;
|
|
708
|
+
border: none;
|
|
709
|
+
border-radius: var(--npt-corner-md, 16px);
|
|
710
|
+
background: var(--md-sys-color-surface-container-high);
|
|
711
|
+
color: var(--md-sys-color-on-surface);
|
|
712
|
+
font-family: var(--npt-font-num);
|
|
713
|
+
font-variant-numeric: tabular-nums;
|
|
714
|
+
font-size: var(--npt-text-headline, 24px);
|
|
715
|
+
cursor: pointer;
|
|
716
|
+
transition: background-color var(--npt-dur-fast, 200ms) var(--npt-ease-standard, ease);
|
|
717
|
+
}
|
|
718
|
+
button:hover {
|
|
719
|
+
background: var(--md-sys-color-surface-container-highest);
|
|
720
|
+
}
|
|
721
|
+
button:active {
|
|
722
|
+
background: var(--md-sys-color-secondary-container);
|
|
723
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
724
|
+
}
|
|
725
|
+
button.action {
|
|
726
|
+
background: transparent;
|
|
727
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
728
|
+
font-family: var(--npt-font-text);
|
|
729
|
+
font-size: var(--npt-text-title, 18px);
|
|
730
|
+
}
|
|
731
|
+
:host([disabled]) .pad {
|
|
732
|
+
opacity: 0.38;
|
|
733
|
+
pointer-events: none;
|
|
734
|
+
}
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
737
|
+
render() {
|
|
738
|
+
const keys = [
|
|
739
|
+
{ key: "1", label: "1", aria: "1" },
|
|
740
|
+
{ key: "2", label: "2", aria: "2" },
|
|
741
|
+
{ key: "3", label: "3", aria: "3" },
|
|
742
|
+
{ key: "4", label: "4", aria: "4" },
|
|
743
|
+
{ key: "5", label: "5", aria: "5" },
|
|
744
|
+
{ key: "6", label: "6", aria: "6" },
|
|
745
|
+
{ key: "7", label: "7", aria: "7" },
|
|
746
|
+
{ key: "8", label: "8", aria: "8" },
|
|
747
|
+
{ key: "9", label: "9", aria: "9" },
|
|
748
|
+
{ key: ".", label: ".", cls: "action", aria: "Decimal point" },
|
|
749
|
+
{ key: "0", label: "0", aria: "0" },
|
|
750
|
+
{ key: "back", label: "⌫", cls: "action", aria: "Backspace" },
|
|
751
|
+
];
|
|
752
|
+
const disabled = this.hasAttribute("disabled") ? "disabled" : "";
|
|
753
|
+
const buttons = keys
|
|
754
|
+
.map((k) => html `<button
|
|
755
|
+
type="button"
|
|
756
|
+
class="${k.cls ?? ""}"
|
|
757
|
+
data-key="${esc(k.key)}"
|
|
758
|
+
aria-label="${esc(k.aria)}"
|
|
759
|
+
${disabled}
|
|
760
|
+
>${esc(k.label)}</button>`)
|
|
761
|
+
.join("");
|
|
762
|
+
return html `<div class="pad" part="pad" role="group" aria-label="Numeric keypad">${buttons}</div>`;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
NptAmountKeypad.observedAttributes = ["value", "disabled"];
|
|
766
|
+
//# sourceMappingURL=money-inputs.js.map
|