@muten/shadcn 0.2.10 → 0.2.12
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/kit.css +16 -4
- package/package.json +1 -1
- package/registry/input-otp.muten +4 -2
- package/registry/kit/i18n.js +1 -1
- package/registry/kit/table.js +5 -3
- package/registry/kit/values.js +14 -3
- package/registry/modal-behavior.js +20 -4
- package/registry.json +1 -1
package/kit.css
CHANGED
|
@@ -606,8 +606,8 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
|
|
|
606
606
|
.cx-otp[data-state="bad"] .cn-input-otp-slot { border-color: var(--destructive); }
|
|
607
607
|
.cx-otp[data-state="bad"] { animation: cx-shake calc(var(--t-move) * 1.35) var(--ease-out); }
|
|
608
608
|
.cx-otp[data-state="ok"] .cn-input-otp-slot { border-color: var(--success); color: var(--success); }
|
|
609
|
-
|
|
610
|
-
|
|
609
|
+
[data-otp-msg][data-tone="bad"] { color: var(--destructive); }
|
|
610
|
+
[data-otp-msg][data-tone="ok"] { color: var(--success); }
|
|
611
611
|
|
|
612
612
|
/* ── Color picker ── */
|
|
613
613
|
.cx-swatch-row { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
@@ -713,7 +713,7 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
|
|
|
713
713
|
.cx-pagination .cx-pg-num .cn-button, .cx-pg-gap { width: 32px; }
|
|
714
714
|
.cx-pg-gap { display: flex; justify-content: center; }
|
|
715
715
|
.cx-pg-list { transition: opacity 160ms ease; }
|
|
716
|
-
.cx-pg-list[data-loading] { opacity: .45; }
|
|
716
|
+
.cx-pg-list[data-loading="true"] { opacity: .45; } /* muten writes "false" too: only true dims */
|
|
717
717
|
.cx-pg-empty { border-top-color: transparent; }
|
|
718
718
|
/* virtual list */
|
|
719
719
|
.cx-vlist { height: 364px; overflow-y: auto; border-radius: calc(var(--radius) * 1.2); box-shadow: 0 0 0 1px var(--border); overscroll-behavior: contain; }
|
|
@@ -780,7 +780,7 @@ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webki
|
|
|
780
780
|
.cx-dt td.cx-dt-act { display: none; }
|
|
781
781
|
.cx-dt-who small { display: none; }
|
|
782
782
|
.cx-dt td.cx-dt-svc[data-when]:not([data-when=""])::after { content: " · " attr(data-when); }
|
|
783
|
-
.cx-dt td.cx-dt-svc ~ td.cx-dt-svc::after { content: none; }
|
|
783
|
+
.cx-dt td.cx-dt-svc ~ td.cx-dt-svc[data-when]::after { content: none; }
|
|
784
784
|
}
|
|
785
785
|
|
|
786
786
|
/* ── Board ── */
|
|
@@ -5548,3 +5548,15 @@ a.cx-bc-item:hover { color: var(--foreground); }
|
|
|
5548
5548
|
|
|
5549
5549
|
/* ── Field: a field that must be filled says so at its label ── */
|
|
5550
5550
|
.cn-label.cx-required::after { content: " *"; color: var(--destructive); }
|
|
5551
|
+
|
|
5552
|
+
/* ── Alert with an action: the action takes its own column (Nova's absolute corner reserves 72 px, and a wider button
|
|
5553
|
+
covered the title); on a phone it goes under the text ── */
|
|
5554
|
+
.style-nova .cn-alert:has(> .cn-alert-action) { padding-right: 10px; grid-template-columns: 1fr auto; }
|
|
5555
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)):has(> .cn-alert-action) { grid-template-columns: auto 1fr auto; }
|
|
5556
|
+
.style-nova .cn-alert > .cn-alert-action { position: static; grid-column: -2; grid-row: 1 / span 9; align-self: center; margin-left: 8px; }
|
|
5557
|
+
@media (max-width: 560px) {
|
|
5558
|
+
.style-nova .cn-alert:has(> .cn-alert-action) { grid-template-columns: 1fr; }
|
|
5559
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)):has(> .cn-alert-action) { grid-template-columns: auto 1fr; }
|
|
5560
|
+
.style-nova .cn-alert > .cn-alert-action { grid-column: 1 / -1; grid-row: auto; justify-self: start; margin: 8px 0 0; }
|
|
5561
|
+
.style-nova .cn-alert:has(> :is(svg, .mu-icon)) > .cn-alert-action { grid-column: 2; }
|
|
5562
|
+
}
|
package/package.json
CHANGED
package/registry/input-otp.muten
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# InputOtp - the verification code: slots in two groups, a caret, “Verificando…”, ok / bad, a resend countdown. The
|
|
2
2
|
# PAGE checks the code: onInput gets it when complete; answer with state ("ok" | "bad") and message.
|
|
3
3
|
# InputOtp(value: code, length: 6, hint: "We sent it to +57 300 123 4567", state: otpState, message: otpMsg, onInput: check)
|
|
4
|
+
# InputOtp(length: 8, letters: true, resendIn: 0, …) a code copied from another screen (KQ4M-7T2X): letters and
|
|
5
|
+
# digits, in capitals, a pasted dash dropped, and no resend button (resendIn 0 never offers one).
|
|
4
6
|
# shadcn-for-muten · Custom (kit values) · host JS input-otp.js.
|
|
5
|
-
part InputOtp(value: text = "", length: number = 6, hint: text = "", state: text = "", message: text = "", resendIn: number = 30, onInput: action) {
|
|
6
|
-
Custom InputOtp inputs(value: $value, length: $length, hint: $hint, state: $state, message: $message, resendIn: $resendIn) on(input: $onInput) class("cx-otp-host")
|
|
7
|
+
part InputOtp(value: text = "", length: number = 6, letters: bool = false, hint: text = "", state: text = "", message: text = "", resendIn: number = 30, onInput: action) {
|
|
8
|
+
Custom InputOtp inputs(value: $value, length: $length, letters: $letters, hint: $hint, state: $state, message: $message, resendIn: $resendIn) on(input: $onInput) class("cx-otp-host")
|
|
7
9
|
}
|
package/registry/kit/i18n.js
CHANGED
|
@@ -90,7 +90,7 @@ Object.assign(ES, {
|
|
|
90
90
|
'maximum': 'máximo', 'minimum': 'mínimo', 'Currency': 'Moneda', 'Country': 'País', '{0} (it has no minor unit)': '{0} (no tiene unidad menor)', 'cents of {0}': 'centavos de {0}',
|
|
91
91
|
'Saved as: {0} {1}': 'Se guarda: {0} {1}', 'Type a price': 'Escribe un precio', 'Search currency': 'Buscar moneda', 'Search country': 'Buscar país',
|
|
92
92
|
'Saved as +{0}': 'Se guarda como +{0}', '{0} digits short for a number from {1}': 'Faltan {0} dígitos para un número de {1}', '{0} digits, without the {1}': '{0} dígitos, sin el {1}',
|
|
93
|
-
'{0}-digit code': 'Código de {0} dígitos', 'Checking…': 'Verificando…', 'Resend in 0:{0}': 'Reenviar en 0:{0}', 'Resend code': 'Reenviar código',
|
|
93
|
+
'{0}-digit code': 'Código de {0} dígitos', '{0}-character code': 'Código de {0} caracteres', 'Checking…': 'Verificando…', 'Resend in 0:{0}': 'Reenviar en 0:{0}', 'Resend code': 'Reenviar código',
|
|
94
94
|
'We sent you a new code': 'Te enviamos un código nuevo', 'Done, it is verified': 'Listo, quedó verificado', 'That is not the code': 'Ese código no es',
|
|
95
95
|
});
|
|
96
96
|
|
package/registry/kit/table.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// and bulk actions are data. Everything else is the artifact's: filters from their type (search · facet · date ·
|
|
3
3
|
// range) with counts, chips that leave with a fade, rows that fade out / slide / enter, the height easing, sort by
|
|
4
4
|
// header, selection with its own bulk bar, the phone's “search + Filters” sheet, and a narrow box becoming Items.
|
|
5
|
-
// columns [{key, label, type: person|text|date|status|money, sub (person: the key under the name), sortable}]
|
|
5
|
+
// columns [{key, label, type: person|text|date|status|money, sub (person: the key under the name), sortable, sort: "desc" (opens newest first)}]
|
|
6
6
|
// rows [{id, …keys}] (a date is "yyyy-mm-ddTHH:MM"; a row's date may be "+N" days from today too)
|
|
7
7
|
// filters [{id, type: search|facet|date|range, label, key | keys (search)}]
|
|
8
8
|
// statuses [{id, label, tone: ok|wait|info|bad}] · noun ["appointment", "appointments"] · bulk [{id, label, variant}]
|
|
@@ -37,14 +37,16 @@ export function dataTable(root, cfg, core, on) {
|
|
|
37
37
|
};
|
|
38
38
|
const SORTERS = Object.fromEntries(COLS.map((c) => [c.key, c.type === 'status' ? (r) => ST_ORDER.indexOf(r[c.key]) : c.type === 'date' ? (r) => +asDate(r[c.key]) : (r) => r[c.key]]));
|
|
39
39
|
const cbHtml = (label, on) => `<button data-slot="checkbox" role="checkbox" data-state="${on ? 'checked' : 'unchecked'}" aria-checked="${on}" class="cn-checkbox" aria-label="${label}"><span data-slot="checkbox-indicator" class="cn-checkbox-indicator"><svg class="cb-mark" viewBox="0 0 24 24">${CB_CHECK}</svg></span></button>`;
|
|
40
|
-
|
|
40
|
+
// the table opens sorted by its date column (else the first); a column with sort: "desc" opens newest first (a log)
|
|
41
|
+
const opening = dateCol ?? COLS[0];
|
|
42
|
+
const firstSort = { key: opening?.key, dir: opening?.sort === 'desc' ? -1 : 1 };
|
|
41
43
|
const bulk = list(cfg.bulk);
|
|
42
44
|
// rows are selectable only when there is something to do with them: without bulk actions, no checkbox column
|
|
43
45
|
const picks = bulk.length > 0;
|
|
44
46
|
// the phone's «Filters» sheet only when there is a filter besides the search
|
|
45
47
|
const sheetFilters = () => FILTERS.some((f) => f.type !== 'search');
|
|
46
48
|
const CELL_CLASS = { person: 'cx-dt-who', text: 'cx-dt-svc', date: 'cx-dt-when', status: 'cx-dt-st', money: 'cx-dt-num' };
|
|
47
|
-
root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="${T('Filters')}"></div><span class="cx-text" data-variant="caption" data-dt-shown aria-live="polite"></span></div><div class="cx-hstack cx-dt-chips" hidden></div><div class="cx-dt-bulk" aria-live="polite"><div><span data-dt-count></span><div class="cx-hstack" style="gap:6px">${bulk.map((b) => ui.button({ label: b.label, variant: b.variant || 'outline', size: 'xs', attrs: `data-dt-bulk="${b.id}"` })).join('')}${ui.button({ label: T('Clear selection'), variant: 'ghost', size: 'xs', attrs: 'data-dt-unselect' })}</div></div></div><div class="cx-dt-scroll cx-scroll"><table class="cx-dt"><thead><tr>${picks ? `<th class="cx-dt-check">${cbHtml(T('Select all'), false).replace('class="cn-checkbox"', 'class="cn-checkbox" data-dt-all')}</th>` : ''}${COLS.map((c) => `<th aria-sort="${c.key === firstSort.key ? 'ascending' : 'none'}" ${c.type === 'money' ? 'class="cx-dt-num"' : ''}>${c.sortable === false ? ui.text(c.label, 'label') : `<button class="cn-button cn-button-variant-ghost cn-button-size-xs cx-th" data-sort="${c.key}">${c.label}<svg><use href="#${c.key === firstSort.key ? 'arrowup' : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
|
|
49
|
+
root.innerHTML = `<div class="cx-dt-host"><div class="cx-dt-toolbar"><div class="cx-hstack cx-dt-tools" role="toolbar" aria-label="${T('Filters')}"></div><span class="cx-text" data-variant="caption" data-dt-shown aria-live="polite"></span></div><div class="cx-hstack cx-dt-chips" hidden></div><div class="cx-dt-bulk" aria-live="polite"><div><span data-dt-count></span><div class="cx-hstack" style="gap:6px">${bulk.map((b) => ui.button({ label: b.label, variant: b.variant || 'outline', size: 'xs', attrs: `data-dt-bulk="${b.id}"` })).join('')}${ui.button({ label: T('Clear selection'), variant: 'ghost', size: 'xs', attrs: 'data-dt-unselect' })}</div></div></div><div class="cx-dt-scroll cx-scroll"><table class="cx-dt"><thead><tr>${picks ? `<th class="cx-dt-check">${cbHtml(T('Select all'), false).replace('class="cn-checkbox"', 'class="cn-checkbox" data-dt-all')}</th>` : ''}${COLS.map((c) => `<th aria-sort="${c.key === firstSort.key ? (firstSort.dir === 1 ? 'ascending' : 'descending') : 'none'}" ${c.type === 'money' ? 'class="cx-dt-num"' : ''}>${c.sortable === false ? ui.text(c.label, 'label') : `<button class="cn-button cn-button-variant-ghost cn-button-size-xs cx-th" data-sort="${c.key}">${c.label}<svg><use href="#${c.key === firstSort.key ? (firstSort.dir === 1 ? 'arrowup' : 'arrowdown') : 'sort'}"/></svg></button>`}</th>`).join('')}</tr></thead><tbody></tbody></table></div></div>`;
|
|
48
50
|
const host = $('.cx-dt-host', root), dtTools = $('.cx-dt-tools', root), dtChips = $('.cx-dt-chips', root), dtCountEl = $('[data-dt-shown]', root), dtBody = $('tbody', root), dtAll = $('[data-dt-all]', root), dtBulk = $('.cx-dt-bulk', root);
|
|
49
51
|
const dtSel = new Set(); let dtSort = firstSort;
|
|
50
52
|
const passes = (r, except) => FILTERS.every((f) => f.id === except || PASS[f.type](f, fState[f.id], r));
|
package/registry/kit/values.js
CHANGED
|
@@ -157,7 +157,14 @@ export function phoneField(root, i, core, on) {
|
|
|
157
157
|
export function otpField(root, i, core, on) {
|
|
158
158
|
const { $ } = core, T = (k, ...a) => core.t(core.langOf(root), k, ...a);
|
|
159
159
|
const len = Number(i.length) || 6, half = Math.ceil(len / 2);
|
|
160
|
-
|
|
160
|
+
// letters: a code people copy from another screen (KQ4M-7T2X), so letters and digits, shown in capitals, and room in the
|
|
161
|
+
// field for a pasted dash or spaces, which are dropped (maxlength would otherwise cut the paste before the cleanup)
|
|
162
|
+
const letters = i.letters === true || i.letters === 'true';
|
|
163
|
+
const keep = letters ? (v) => v.toUpperCase().replace(/[^A-Z0-9]/g, '') : (v) => v.replace(/\D/g, '');
|
|
164
|
+
const field = letters
|
|
165
|
+
? `inputmode="text" autocapitalize="characters" autocomplete="off" spellcheck="false" maxlength="${len * 2}" aria-label="${T('{0}-character code', len)}"`
|
|
166
|
+
: `inputmode="numeric" autocomplete="one-time-code" maxlength="${len}" aria-label="${T('{0}-digit code', len)}"`;
|
|
167
|
+
root.innerHTML = `<div class="cx-vline cx-otp-stack"><div class="cx-otp"><input class="cx-otp-input" ${field}><div data-slot="input-otp" class="cn-input-otp" aria-hidden="true"><div data-slot="input-otp-group" class="cn-input-otp-group"></div><div data-slot="input-otp-separator" class="cn-input-otp-separator cx-otp-sep"><span></span></div><div data-slot="input-otp-group" class="cn-input-otp-group"></div></div></div><p class="cx-note" data-otp-msg></p><button data-slot="button" class="cn-button cn-button-variant-link cn-button-size-sm group/button" data-otp-resend disabled></button></div>`;
|
|
161
168
|
const otpIn = $('.cx-otp-input', root), otpBox = $('.cx-otp', root), otpMsg = $('[data-otp-msg]', root), resend = $('[data-otp-resend]', root);
|
|
162
169
|
const slots = [];
|
|
163
170
|
root.querySelectorAll('.cn-input-otp-group').forEach((g, gi) => { const n = gi ? len - half : half; for (let k = 0; k < n; k++) { const s = document.createElement('div'); s.className = 'cn-input-otp-slot'; s.dataset.slot = 'input-otp-slot'; g.appendChild(s); slots.push(s); } });
|
|
@@ -166,16 +173,20 @@ export function otpField(root, i, core, on) {
|
|
|
166
173
|
const paint = () => { const v = otpIn.value, focused = document.activeElement === otpIn, at = Math.min(v.length, len - 1); slots.forEach((s, k) => { s.textContent = v[k] || ''; const act = focused && k === at && !busy; s.dataset.active = act; if (act && !v[k]) s.innerHTML = '<span class="cx-caret"></span>'; }); };
|
|
167
174
|
const toEnd = () => { const n = otpIn.value.length; otpIn.setSelectionRange(n, n); };
|
|
168
175
|
otpIn.addEventListener('input', () => {
|
|
169
|
-
otpIn.value = otpIn.value
|
|
176
|
+
otpIn.value = keep(otpIn.value).slice(0, len); delete otpBox.dataset.state; otpMsg.removeAttribute('data-tone'); otpMsg.textContent = hint; paint();
|
|
170
177
|
if (otpIn.value.length === len) { busy = true; otpIn.readOnly = true; otpMsg.textContent = T('Checking…'); paint(); on?.complete?.(otpIn.value); }
|
|
171
178
|
});
|
|
172
179
|
['focus', 'blur', 'click', 'keyup'].forEach((t) => otpIn.addEventListener(t, () => { toEnd(); paint(); }));
|
|
173
180
|
otpIn.addEventListener('select', toEnd);
|
|
181
|
+
// resendIn 0: a code that is not sent again (one a shop gave you, one a terminal shows), so no resend button at all
|
|
174
182
|
let left = Number(i.resendIn ?? 30), rt = 0;
|
|
183
|
+
const resends = left > 0;
|
|
184
|
+
resend.hidden = !resends;
|
|
175
185
|
const tick = () => { left--; resend.disabled = left > 0; resend.textContent = left > 0 ? T('Resend in 0:{0}', String(left).padStart(2, '0')) : T('Resend code'); if (left > 0) rt = setTimeout(tick, 1000); };
|
|
176
186
|
resend.textContent = T('Resend in 0:{0}', String(left).padStart(2, '0'));
|
|
177
187
|
resend.onclick = () => { left = Number(i.resendIn ?? 30) + 1; clearTimeout(rt); tick(); otpMsg.removeAttribute('data-tone'); otpMsg.textContent = T('We sent you a new code'); otpIn.value = ''; delete otpBox.dataset.state; otpIn.focus(); on?.resend?.(); };
|
|
178
|
-
rt = setTimeout(tick, 1000);
|
|
188
|
+
if (resends) rt = setTimeout(tick, 1000);
|
|
189
|
+
paint();
|
|
179
190
|
const answer = (st, msg) => {
|
|
180
191
|
if (!st || st === 'checking') return;
|
|
181
192
|
busy = false; otpIn.readOnly = false;
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
// the Modal part's (muten state + kit.css). This only does what markup cannot: history (Back closes), Esc, the focus
|
|
3
3
|
// trap, the scroll lock, the auto mode (dialog on a wide screen, drawer on a phone) and the drawer's drag to close.
|
|
4
4
|
// Same rules as the reference artifact's modal.
|
|
5
|
+
// MODALS STACK (a receipt opened over a cita): Back, Esc and the focus trap belong to the TOP one only, and the page's
|
|
6
|
+
// scroll is unlocked when the last one closes. Each modal used to listen to every Back: closing the receipt went back
|
|
7
|
+
// in history and that Back also closed the cita under it, leaving the agenda without its buttons (21-10-2026).
|
|
8
|
+
const stack = (globalThis.__cxModals ||= { list: [], skip: 0, wired: false });
|
|
9
|
+
if (!stack.wired) {
|
|
10
|
+
stack.wired = true;
|
|
11
|
+
addEventListener('popstate', () => {
|
|
12
|
+
// the history.back() a modal makes when it closes itself is its own, not a Back for the one under it
|
|
13
|
+
if (stack.skip > 0) { stack.skip -= 1; return; }
|
|
14
|
+
stack.list.at(-1)?.onPop();
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
const isTop = (me) => stack.list.at(-1) === me;
|
|
18
|
+
|
|
5
19
|
export function mount(el, inputs, on) {
|
|
6
20
|
const mdl = el.closest('.cx-modal'), root = mdl.parentElement, ov = root.querySelector(':scope > .cx-overlay');
|
|
7
21
|
const body = mdl.querySelector(':scope > .cx-modal-body');
|
|
@@ -11,6 +25,7 @@ export function mount(el, inputs, on) {
|
|
|
11
25
|
// of opening: a dialog that only learns it is a dialog as it opens animates its centring from the corner
|
|
12
26
|
const resolve = () => { if (auto && !isOpen) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog'; };
|
|
13
27
|
let isOpen = false, pushed = false, lastFocus = null;
|
|
28
|
+
const me = { onPop: () => { if (isOpen) { pushed = false; close(); } } };
|
|
14
29
|
queueMicrotask(() => resolve()); phone.addEventListener('change', resolve);
|
|
15
30
|
// the footer sticks under the scrolling body, not inside it
|
|
16
31
|
const placeFooter = () => { const f = body.querySelector('.cx-modal-foot'); if (f) mdl.appendChild(f); };
|
|
@@ -40,6 +55,7 @@ export function mount(el, inputs, on) {
|
|
|
40
55
|
if (auto) mdl.dataset.mode = phone.matches ? 'drawer' : 'dialog';
|
|
41
56
|
if (wantsAnchor()) place(); else unplace();
|
|
42
57
|
lastFocus = document.activeElement;
|
|
58
|
+
stack.list.push(me);
|
|
43
59
|
document.documentElement.classList.add('cx-locked');
|
|
44
60
|
// on the phone, focusing a field would throw the keyboard over the sheet: focus the sheet itself
|
|
45
61
|
requestAnimationFrame(() => { ((mdl.dataset.mode === 'dialog' && focusables().find((x) => x.matches('input, textarea'))) || mdl).focus({ preventScroll: true }); });
|
|
@@ -48,9 +64,10 @@ export function mount(el, inputs, on) {
|
|
|
48
64
|
const shut = () => {
|
|
49
65
|
if (!isOpen) return; isOpen = false;
|
|
50
66
|
mdl.style.transform = ''; if (ov) ov.style.opacity = '';
|
|
51
|
-
|
|
67
|
+
stack.list = stack.list.filter((one) => one !== me);
|
|
68
|
+
if (stack.list.length === 0) document.documentElement.classList.remove('cx-locked');
|
|
52
69
|
lastFocus?.focus?.({ preventScroll: true });
|
|
53
|
-
if (pushed) { pushed = false; history.back(); }
|
|
70
|
+
if (pushed) { pushed = false; stack.skip += 1; history.back(); }
|
|
54
71
|
};
|
|
55
72
|
mdl.tabIndex = -1;
|
|
56
73
|
// A link inside (a menu in a drawer): close first, then go. Closing undoes the history entry the modal pushed, and
|
|
@@ -64,9 +81,8 @@ export function mount(el, inputs, on) {
|
|
|
64
81
|
pushed = false; addEventListener('popstate', go);
|
|
65
82
|
close(); history.back();
|
|
66
83
|
}, true);
|
|
67
|
-
addEventListener('popstate', () => { if (isOpen) { pushed = false; close(); } });
|
|
68
84
|
document.addEventListener('keydown', (e) => {
|
|
69
|
-
if (!isOpen) return;
|
|
85
|
+
if (!isOpen || !isTop(me)) return;
|
|
70
86
|
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
71
87
|
if (e.key === 'Tab') { const f = focusables(); if (!f.length) return; const first = f[0], last = f[f.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }
|
|
72
88
|
});
|
package/registry.json
CHANGED
|
@@ -457,7 +457,7 @@
|
|
|
457
457
|
"part": "InputOtp",
|
|
458
458
|
"file": "registry/input-otp.muten",
|
|
459
459
|
"component": "InputOtp",
|
|
460
|
-
"description": "InputOtp(value, length, hint, state, message, resendIn, onInput): slots, caret, ok/bad, resend countdown."
|
|
460
|
+
"description": "InputOtp(value, length, letters, hint, state, message, resendIn, onInput): slots, caret, ok/bad, resend countdown (none with resendIn 0); letters for a code like KQ4M-7T2X."
|
|
461
461
|
},
|
|
462
462
|
{
|
|
463
463
|
"name": "input",
|