/** * createPhoneInput - the HEADLESS core behind : a runes-based * state machine that owns the country dial-code + national-number state, parsing * an E.164-ish string (`+`) and re-emitting it. Reuses the pure * `./countries` reference and `./datetime/mask` engine. Exposed as **prop-getters** * you spread onto YOUR OWN markup. No styles, no DOM. * * ```svelte * * * * ``` * * The styled is just one renderer over this core. */ import { COUNTRIES, COUNTRY_BY_CODE, flagEmoji, phoneDigitsValid } from './countries' import { applyMask } from './datetime/mask' import { editorAria, type EditorAriaState } from './editor-contract' /** Emitted alongside the phone value: parsed parts + per-country validity. */ export type PhoneParts = { country: string; dial: string; national: string; valid: boolean; complete: boolean } /** Reactive inputs are passed as getters so the core tracks live prop changes. */ export type PhoneInputConfig = { value: () => string onChange?: (value: string, parts: PhoneParts) => void /** Default country ISO code. */ country?: () => string disabled?: () => boolean readonly?: () => boolean placeholder?: () => string | undefined /** Accessible name for the country `` element. */ selectProps: () => ({ value: iso, disabled: disabled(), 'aria-label': config.countryLabel?.() ?? 'Country', onchange: onCountryChange, }), /** Spread onto the national number input element. */ inputProps: () => ({ id: config.id?.(), value: displayNational, type: 'tel' as const, inputmode: 'tel' as const, placeholder: config.placeholder?.() ?? 'Phone number', disabled: disabled(), readonly: readonly(), // aria-invalid/required/describedby come from the editor contract; the // aria-label default stays 'Phone number' so the field is always named. ...editorAria(ariaState()), 'aria-label': config.ariaLabel?.() ?? 'Phone number', oninput: onNumberInput, }), } } export type PhoneInputCore = ReturnType