This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
@@ -0,0 +1,36 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { PinInputCellProps } from "../types.js";
import { PinInputCellState } from "../pin-input.svelte.js";
import { createId } from "../../../internal/create-id.js";
const uid = $props.id();
let {
id = createId(uid),
ref = $bindable(null),
cell,
child,
children,
...restProps
}: PinInputCellProps = $props();
const cellState = PinInputCellState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
cell: boxWith(() => cell),
});
const mergedProps = $derived(mergeProps(restProps, cellState.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
@@ -0,0 +1,4 @@
import type { PinInputCellProps } from "../types.js";
declare const PinInputCell: import("svelte").Component<PinInputCellProps, {}, "ref">;
type PinInputCell = ReturnType<typeof PinInputCell>;
export default PinInputCell;
+66
View File
@@ -0,0 +1,66 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { PinInputRootProps } from "../types.js";
import { PinInputRootState } from "../pin-input.svelte.js";
import { createId } from "../../../internal/create-id.js";
import { noop } from "../../../internal/noop.js";
const uid = $props.id();
let {
id = createId(uid),
inputId = `${createId(uid)}-input`,
ref = $bindable(null),
maxlength = 6,
textalign = "left",
pattern,
inputmode = "numeric",
onComplete = noop,
pushPasswordManagerStrategy = "increase-width",
class: containerClass = "",
children,
autocomplete = "one-time-code",
disabled = false,
value = $bindable(""),
onValueChange = noop,
pasteTransformer,
...restProps
}: PinInputRootProps = $props();
const rootState = PinInputRootState.create({
id: boxWith(() => id),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
inputId: boxWith(() => inputId),
autocomplete: boxWith(() => autocomplete),
maxLength: boxWith(() => maxlength),
textAlign: boxWith(() => textalign),
disabled: boxWith(() => disabled),
inputmode: boxWith(() => inputmode),
pattern: boxWith(() => pattern),
onComplete: boxWith(() => onComplete),
value: boxWith(
() => value,
(v) => {
value = v;
onValueChange(v);
}
),
pushPasswordManagerStrategy: boxWith(() => pushPasswordManagerStrategy),
pasteTransformer: boxWith(() => pasteTransformer),
});
const mergedInputProps = $derived(mergeProps(restProps, rootState.inputProps));
const mergedRootProps = $derived(mergeProps(rootState.rootProps, { class: containerClass }));
const mergedInputWrapperProps = $derived(mergeProps(rootState.inputWrapperProps, {}));
</script>
<div {...mergedRootProps}>
{@render children?.(rootState.snippetProps)}
<div {...mergedInputWrapperProps}>
<input {...mergedInputProps} />
</div>
</div>
@@ -0,0 +1,4 @@
import type { PinInputRootProps } from "../types.js";
declare const PinInput: import("svelte").Component<PinInputRootProps, {}, "value" | "ref">;
type PinInput = ReturnType<typeof PinInput>;
export default PinInput;
+3
View File
@@ -0,0 +1,3 @@
export { default as Root } from "./components/pin-input.svelte";
export { default as Cell } from "./components/pin-input-cell.svelte";
export type { PinInputRootProps as RootProps, PinInputCellProps as CellProps } from "./types.js";
+2
View File
@@ -0,0 +1,2 @@
export { default as Root } from "./components/pin-input.svelte";
export { default as Cell } from "./components/pin-input-cell.svelte";
+1
View File
@@ -0,0 +1 @@
export * as PinInput from "./exports.js";
+1
View File
@@ -0,0 +1 @@
export * as PinInput from "./exports.js";
File diff suppressed because one or more lines are too long
+431
View File
@@ -0,0 +1,431 @@
import { Previous, watch } from "runed";
import { onMount } from "svelte";
import { attachRef, DOMContext, simpleBox, } from "svelte-toolbelt";
import { usePasswordManagerBadge } from "./usePasswordManager.svelte.js";
import { createBitsAttrs, boolToTrueOrUndef } from "../../internal/attrs.js";
import { on } from "svelte/events";
export const REGEXP_ONLY_DIGITS = "^\\d+$";
export const REGEXP_ONLY_CHARS = "^[a-zA-Z]+$";
export const REGEXP_ONLY_DIGITS_AND_CHARS = "^[a-zA-Z0-9]+$";
const pinInputAttrs = createBitsAttrs({
component: "pin-input",
parts: ["root", "cell"],
});
const KEYS_TO_IGNORE = [
"Backspace",
"Delete",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
"Escape",
"Enter",
"Tab",
"Shift",
"Control",
"Meta",
];
export class PinInputRootState {
static create(opts) {
return new PinInputRootState(opts);
}
opts;
attachment;
#inputRef = simpleBox(null);
#isHoveringInput = $state(false);
inputAttachment = attachRef(this.#inputRef);
#isFocused = simpleBox(false);
#mirrorSelectionStart = $state(null);
#mirrorSelectionEnd = $state(null);
#previousValue = new Previous(() => this.opts.value.current ?? "");
#regexPattern = $derived.by(() => {
if (typeof this.opts.pattern.current === "string") {
return new RegExp(this.opts.pattern.current);
}
else {
return this.opts.pattern.current;
}
});
#prevInputMetadata = $state({
prev: [null, null, "none"],
willSyntheticBlur: false,
});
#pwmb;
#initialLoad;
domContext;
constructor(opts) {
this.opts = opts;
this.attachment = attachRef(this.opts.ref);
this.domContext = new DOMContext(opts.ref);
this.#initialLoad = {
value: this.opts.value,
isIOS: typeof window !== "undefined" &&
window?.CSS?.supports("-webkit-touch-callout", "none"),
};
this.#pwmb = usePasswordManagerBadge({
containerRef: this.opts.ref,
inputRef: this.#inputRef,
isFocused: this.#isFocused,
pushPasswordManagerStrategy: this.opts.pushPasswordManagerStrategy,
domContext: this.domContext,
});
onMount(() => {
const input = this.#inputRef.current;
const container = this.opts.ref.current;
if (!input || !container)
return;
if (this.#initialLoad.value.current !== input.value) {
this.opts.value.current = input.value;
}
this.#prevInputMetadata.prev = [
input.selectionStart,
input.selectionEnd,
input.selectionDirection ?? "none",
];
const unsub = on(this.domContext.getDocument(), "selectionchange", this.#onDocumentSelectionChange, {
capture: true,
});
this.#onDocumentSelectionChange();
if (this.domContext.getActiveElement() === input) {
this.#isFocused.current = true;
}
if (!this.domContext.getElementById("pin-input-style")) {
this.#applyStyles();
}
const updateRootHeight = () => {
if (container) {
container.style.setProperty("--bits-pin-input-root-height", `${input.clientHeight}px`);
}
};
updateRootHeight();
const resizeObserver = new ResizeObserver(updateRootHeight);
resizeObserver.observe(input);
return () => {
unsub();
resizeObserver.disconnect();
};
});
watch([() => this.opts.value.current, () => this.#inputRef.current], () => {
syncTimeouts(() => {
const input = this.#inputRef.current;
if (!input)
return;
// forcefully remove :autofill state
input.dispatchEvent(new Event("input"));
// update selection state
const start = input.selectionStart;
const end = input.selectionEnd;
const dir = input.selectionDirection ?? "none";
if (start !== null && end !== null) {
this.#mirrorSelectionStart = start;
this.#mirrorSelectionEnd = end;
this.#prevInputMetadata.prev = [start, end, dir];
}
}, this.domContext);
});
$effect(() => {
// invoke `onComplete` when the input is completely filled.
const value = this.opts.value.current;
const prevValue = this.#previousValue.current;
const maxLength = this.opts.maxLength.current;
const onComplete = this.opts.onComplete.current;
if (prevValue === undefined)
return;
if (value !== prevValue && prevValue.length < maxLength && value.length === maxLength) {
onComplete(value);
}
});
}
onkeydown = (e) => {
const key = e.key;
if (KEYS_TO_IGNORE.includes(key))
return;
// if ctrl or cmd is pressed, they are likely to be shortcuts and should not be tested
// against the regex
if (e.ctrlKey || e.metaKey)
return;
if (key && this.#regexPattern && !this.#regexPattern.test(key)) {
e.preventDefault();
}
};
#rootStyles = $derived.by(() => ({
position: "relative",
cursor: this.opts.disabled.current ? "default" : "text",
userSelect: "none",
WebkitUserSelect: "none",
pointerEvents: "none",
}));
rootProps = $derived.by(() => ({
id: this.opts.id.current,
[pinInputAttrs.root]: "",
style: this.#rootStyles,
...this.attachment,
}));
inputWrapperProps = $derived.by(() => ({
style: {
position: "absolute",
inset: 0,
pointerEvents: "none",
},
}));
#inputStyle = $derived.by(() => ({
position: "absolute",
inset: 0,
width: this.#pwmb.willPushPwmBadge
? `calc(100% + ${this.#pwmb.PWM_BADGE_SPACE_WIDTH})`
: "100%",
clipPath: this.#pwmb.willPushPwmBadge
? `inset(0 ${this.#pwmb.PWM_BADGE_SPACE_WIDTH} 0 0)`
: undefined,
height: "100%",
display: "flex",
textAlign: this.opts.textAlign.current,
opacity: "1",
color: "transparent",
pointerEvents: "all",
background: "transparent",
caretColor: "transparent",
border: "0 solid transparent",
outline: "0 solid transparent",
boxShadow: "none",
lineHeight: "1",
letterSpacing: "-.5em",
fontSize: "var(--bits-pin-input-root-height)",
fontFamily: "monospace",
fontVariantNumeric: "tabular-nums",
}));
#applyStyles() {
const doc = this.domContext.getDocument();
const styleEl = doc.createElement("style");
styleEl.id = "pin-input-style";
doc.head.appendChild(styleEl);
if (styleEl.sheet) {
const autoFillStyles = "background: transparent !important; color: transparent !important; border-color: transparent !important; opacity: 0 !important; box-shadow: none !important; -webkit-box-shadow: none !important; -webkit-text-fill-color: transparent !important;";
safeInsertRule(styleEl.sheet, "[data-pin-input-input]::selection { background: transparent !important; color: transparent !important; }");
safeInsertRule(styleEl.sheet, `[data-pin-input-input]:autofill { ${autoFillStyles} }`);
safeInsertRule(styleEl.sheet, `[data-pin-input-input]:-webkit-autofill { ${autoFillStyles} }`);
// iOS
safeInsertRule(styleEl.sheet, `@supports (-webkit-touch-callout: none) { [data-pin-input-input] { letter-spacing: -.6em !important; font-weight: 100 !important; font-stretch: ultra-condensed; font-optical-sizing: none !important; left: -1px !important; right: 1px !important; } }`);
// PWM badges
safeInsertRule(styleEl.sheet, `[data-pin-input-input] + * { pointer-events: all !important; }`);
}
}
#onDocumentSelectionChange = () => {
const input = this.#inputRef.current;
const container = this.opts.ref.current;
if (!input || !container)
return;
if (this.domContext.getActiveElement() !== input) {
this.#mirrorSelectionStart = null;
this.#mirrorSelectionEnd = null;
return;
}
const selStart = input.selectionStart;
const selEnd = input.selectionEnd;
const selDir = input.selectionDirection ?? "none";
const maxLength = input.maxLength;
const val = input.value;
const prev = this.#prevInputMetadata.prev;
let start = -1;
let end = -1;
let direction;
if (val.length !== 0 && selStart !== null && selEnd !== null) {
const isSingleCaret = selStart === selEnd;
const isInsertMode = selStart === val.length && val.length < maxLength;
if (isSingleCaret && !isInsertMode) {
const c = selStart;
if (c === 0) {
start = 0;
end = 1;
direction = "forward";
}
else if (c === maxLength) {
start = c - 1;
end = c;
direction = "backward";
}
else if (maxLength > 1 && val.length > 1) {
let offset = 0;
if (prev[0] !== null && prev[1] !== null) {
direction = c < prev[1] ? "backward" : "forward";
const wasPreviouslyInserting = prev[0] === prev[1] && prev[0] < maxLength;
if (direction === "backward" && !wasPreviouslyInserting) {
offset = -1;
}
}
start = offset + c;
end = offset + c + 1;
}
}
if (start !== -1 && end !== -1 && start !== end) {
this.#inputRef.current?.setSelectionRange(start, end, direction);
}
}
// finally update the state
const s = start !== -1 ? start : selStart;
const e = end !== -1 ? end : selEnd;
const dir = direction ?? selDir;
this.#mirrorSelectionStart = s;
this.#mirrorSelectionEnd = e;
this.#prevInputMetadata.prev = [s, e, dir];
};
oninput = (e) => {
const newValue = e.currentTarget.value.slice(0, this.opts.maxLength.current);
if (newValue.length > 0 && this.#regexPattern && !this.#regexPattern.test(newValue)) {
e.preventDefault();
return;
}
const maybeHasDeleted = typeof this.#previousValue.current === "string" &&
newValue.length < this.#previousValue.current.length;
if (maybeHasDeleted) {
// Since cutting/deleting text doesn't trigger
// selectionchange event, we'll have to dispatch it manually.
// NOTE: The following line also triggers when cmd+A then pasting
// a value with smaller length, which is not ideal for performance.
this.domContext.getDocument().dispatchEvent(new Event("selectionchange"));
}
this.opts.value.current = newValue;
};
onfocus = (_) => {
const input = this.#inputRef.current;
if (input) {
const start = Math.min(input.value.length, this.opts.maxLength.current - 1);
const end = input.value.length;
input.setSelectionRange(start, end);
this.#mirrorSelectionStart = start;
this.#mirrorSelectionEnd = end;
}
this.#isFocused.current = true;
};
onpaste = (e) => {
const input = this.#inputRef.current;
if (!input)
return;
const getNewValue = (finalContent) => {
const start = input.selectionStart === null ? undefined : input.selectionStart;
const end = input.selectionEnd === null ? undefined : input.selectionEnd;
const isReplacing = start !== end;
const initNewVal = this.opts.value.current;
const newValueUncapped = isReplacing
? initNewVal.slice(0, start) + finalContent + initNewVal.slice(end)
: initNewVal.slice(0, start) + finalContent + initNewVal.slice(start);
return newValueUncapped.slice(0, this.opts.maxLength.current);
};
const isValueInvalid = (newValue) => {
return newValue.length > 0 && this.#regexPattern && !this.#regexPattern.test(newValue);
};
if (!this.opts.pasteTransformer?.current &&
(!this.#initialLoad.isIOS || !e.clipboardData || !input)) {
const newValue = getNewValue(e.clipboardData?.getData("text/plain"));
if (isValueInvalid(newValue)) {
e.preventDefault();
}
return;
}
const _content = e.clipboardData?.getData("text/plain") ?? "";
const content = this.opts.pasteTransformer?.current
? this.opts.pasteTransformer.current(_content)
: _content;
e.preventDefault();
const newValue = getNewValue(content);
if (isValueInvalid(newValue))
return;
input.value = newValue;
this.opts.value.current = newValue;
const selStart = Math.min(newValue.length, this.opts.maxLength.current - 1);
const selEnd = newValue.length;
input.setSelectionRange(selStart, selEnd);
this.#mirrorSelectionStart = selStart;
this.#mirrorSelectionEnd = selEnd;
};
onmouseover = (_) => {
this.#isHoveringInput = true;
};
onmouseleave = (_) => {
this.#isHoveringInput = false;
};
onblur = (_) => {
if (this.#prevInputMetadata.willSyntheticBlur) {
this.#prevInputMetadata.willSyntheticBlur = false;
return;
}
this.#isFocused.current = false;
};
inputProps = $derived.by(() => ({
id: this.opts.inputId.current,
style: this.#inputStyle,
autocomplete: this.opts.autocomplete.current || "one-time-code",
"data-pin-input-input": "",
"data-pin-input-input-mss": this.#mirrorSelectionStart,
"data-pin-input-input-mse": this.#mirrorSelectionEnd,
inputmode: this.opts.inputmode.current,
pattern: this.#regexPattern?.source,
maxlength: this.opts.maxLength.current,
value: this.opts.value.current,
disabled: boolToTrueOrUndef(this.opts.disabled.current),
//
onpaste: this.onpaste,
oninput: this.oninput,
onkeydown: this.onkeydown,
onmouseover: this.onmouseover,
onmouseleave: this.onmouseleave,
onfocus: this.onfocus,
onblur: this.onblur,
...this.inputAttachment,
}));
#cells = $derived.by(() => Array.from({ length: this.opts.maxLength.current }).map((_, idx) => {
const isActive = this.#isFocused.current &&
this.#mirrorSelectionStart !== null &&
this.#mirrorSelectionEnd !== null &&
((this.#mirrorSelectionStart === this.#mirrorSelectionEnd &&
idx === this.#mirrorSelectionStart) ||
(idx >= this.#mirrorSelectionStart && idx < this.#mirrorSelectionEnd));
const char = this.opts.value.current[idx] !== undefined ? this.opts.value.current[idx] : null;
return {
char,
isActive,
hasFakeCaret: isActive && char === null,
};
}));
snippetProps = $derived.by(() => ({
cells: this.#cells,
isFocused: this.#isFocused.current,
isHovering: this.#isHoveringInput,
}));
}
export class PinInputCellState {
static create(opts) {
return new PinInputCellState(opts);
}
opts;
attachment;
constructor(opts) {
this.opts = opts;
this.attachment = attachRef(this.opts.ref);
}
props = $derived.by(() => ({
id: this.opts.id.current,
[pinInputAttrs.cell]: "",
"data-active": this.opts.cell.current.isActive ? "" : undefined,
"data-inactive": !this.opts.cell.current.isActive ? "" : undefined,
...this.attachment,
}));
}
// oxlint-disable-next-line no-explicit-any
export function syncTimeouts(cb, domContext) {
const t1 = domContext.setTimeout(cb, 0); // For faster machines
const t2 = domContext.setTimeout(cb, 1_0);
const t3 = domContext.setTimeout(cb, 5_0);
return [t1, t2, t3];
}
function safeInsertRule(sheet, rule) {
try {
sheet.insertRule(rule);
}
catch {
// oxlint-disable-next-line no-console
console.error("pin input could not insert CSS rule:", rule);
}
}
+82
View File
@@ -0,0 +1,82 @@
import type { Snippet } from "svelte";
import type { OnChangeFn, WithChild, Without } from "../../internal/types.js";
import type { BitsPrimitiveDivAttributes, BitsPrimitiveInputAttributes } from "../../shared/attributes.js";
export type PinInputRootSnippetProps = {
cells: PinInputCell[];
isFocused: boolean;
isHovering: boolean;
};
export type PinInputRootPropsWithoutHTML = Omit<WithChild<{
/**
* The value of the input.
*
* @bindable
*/
value?: string;
/**
* A callback function that is called when the value of the input changes.
*/
onValueChange?: OnChangeFn<string>;
/**
* A callback function that is called when the user pastes text into the input.
* It receives the pasted text as an argument, and should return the sanitized text.
*
* Use this function to clean up the pasted text, like removing hyphens or other
* characters that should not make it into the input.
*/
pasteTransformer?: (text: string) => string;
/**
* The max length of the input.
*/
maxlength: number;
/**
* Customize the alignment of the text within in the input.
*
* @default "left"
*/
textalign?: "left" | "center" | "right";
/**
* A callback function that is called when the input is completely filled.
*
*/
onComplete?: (...args: any[]) => void;
/**
* How to handle the input when a password manager is detected.
*/
pushPasswordManagerStrategy?: "increase-width" | "none";
/**
* Whether the input is disabled
*/
disabled?: boolean;
/**
* Optionally provide an ID to apply to the hidden input element.
*/
inputId?: string;
/**
* The children snippet used to render the individual cells.
*/
children: Snippet<[PinInputRootSnippetProps]>;
}, PinInputRootSnippetProps>, "child">;
export type PinInputRootProps = PinInputRootPropsWithoutHTML & Without<BitsPrimitiveInputAttributes, PinInputRootPropsWithoutHTML>;
export type PinInputCellPropsWithoutHTML = WithChild<{
/**
* This specific cell, which is provided by the `cells` snippet prop from
* the `PinInput.Root` component.
*/
cell: PinInputCell;
}>;
export type PinInputCellProps = PinInputCellPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, PinInputCellPropsWithoutHTML>;
export type PinInputCell = {
/**
* The character displayed in the cell.
*/
char: string | null | undefined;
/**
* Whether the cell is active.
*/
isActive: boolean;
/**
* Whether the cell has a fake caret.
*/
hasFakeCaret: boolean;
};
+1
View File
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,15 @@
import { type DOMContext, type ReadableBox, type WritableBox } from "svelte-toolbelt";
import type { PinInputRootPropsWithoutHTML } from "./types.js";
type UsePasswordManagerBadgeProps = {
containerRef: WritableBox<HTMLElement | null>;
inputRef: WritableBox<HTMLInputElement | null>;
pushPasswordManagerStrategy: ReadableBox<PinInputRootPropsWithoutHTML["pushPasswordManagerStrategy"]>;
isFocused: ReadableBox<boolean>;
domContext: DOMContext;
};
export declare function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, domContext, }: UsePasswordManagerBadgeProps): {
readonly hasPwmBadge: boolean;
readonly willPushPwmBadge: boolean;
PWM_BADGE_SPACE_WIDTH: "40px";
};
export {};
+90
View File
@@ -0,0 +1,90 @@
import { getWindow } from "svelte-toolbelt";
const PWM_BADGE_MARGIN_RIGHT = 18;
const PWM_BADGE_SPACE_WIDTH_PX = 40;
const PWM_BADGE_SPACE_WIDTH = `${PWM_BADGE_SPACE_WIDTH_PX}px`;
const PASSWORD_MANAGER_SELECTORS = [
"[data-lastpass-icon-root]", // LastPass,
"com-1password-button", // 1Password,
"[data-dashlanecreated]", // Dashlane,
'[style$="2147483647 !important;"]', // Bitwarden
].join(",");
export function usePasswordManagerBadge({ containerRef, inputRef, pushPasswordManagerStrategy, isFocused, domContext, }) {
let hasPwmBadge = $state(false);
let hasPwmBadgeSpace = $state(false);
let done = $state(false);
function willPushPwmBadge() {
const strategy = pushPasswordManagerStrategy.current;
if (strategy === "none")
return false;
const increaseWidthCase = strategy === "increase-width" && hasPwmBadge && hasPwmBadgeSpace;
return increaseWidthCase;
}
function trackPwmBadge() {
const container = containerRef.current;
const input = inputRef.current;
if (!container || !input || done || pushPasswordManagerStrategy.current === "none")
return;
const elementToCompare = container;
// get the top right-center point of the container
// that is usually where most password managers place their badge
const rightCornerX = elementToCompare.getBoundingClientRect().left + elementToCompare.offsetWidth;
const centeredY = elementToCompare.getBoundingClientRect().top + elementToCompare.offsetHeight / 2;
const x = rightCornerX - PWM_BADGE_MARGIN_RIGHT;
const y = centeredY;
// do an extra search to check for all the password manager badges
const passwordManagerStrategy = domContext.querySelectorAll(PASSWORD_MANAGER_SELECTORS);
// if no password manager is detected, dispatch document.elementFromPoint to
// identify the badges
if (passwordManagerStrategy.length === 0) {
const maybeBadgeEl = domContext.getDocument().elementFromPoint(x, y);
// if the found element is the container,
// then it is not a badge, most times there is no badge in this case
if (maybeBadgeEl === container)
return;
}
hasPwmBadge = true;
done = true;
}
$effect(() => {
const container = containerRef.current;
if (!container || pushPasswordManagerStrategy.current === "none")
return;
// check if the pwm area is fully visible
function checkHasSpace() {
const viewportWidth = getWindow(container).innerWidth;
const distanceToRightEdge = viewportWidth - container.getBoundingClientRect().right;
hasPwmBadgeSpace = distanceToRightEdge >= PWM_BADGE_SPACE_WIDTH_PX;
}
checkHasSpace();
const interval = setInterval(checkHasSpace, 1000);
return () => {
clearInterval(interval);
};
});
$effect(() => {
const focused = isFocused.current || domContext.getActiveElement() === inputRef.current;
if (pushPasswordManagerStrategy.current === "none" || !focused)
return;
const t1 = setTimeout(trackPwmBadge, 0);
const t2 = setTimeout(trackPwmBadge, 2000);
const t3 = setTimeout(trackPwmBadge, 5000);
const t4 = setTimeout(() => {
done = true;
}, 6000);
return () => {
clearTimeout(t1);
clearTimeout(t2);
clearTimeout(t3);
clearTimeout(t4);
};
});
return {
get hasPwmBadge() {
return hasPwmBadge;
},
get willPushPwmBadge() {
return willPushPwmBadge();
},
PWM_BADGE_SPACE_WIDTH,
};
}