@loworbitstudio/visor 1.22.0 → 1.24.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.
@@ -1930,11 +1930,12 @@
1930
1930
  {
1931
1931
  "name": "phone-input",
1932
1932
  "type": "registry:ui",
1933
- "description": "An international phone number input with country code selection, geo-IP detection, format-as-you-type, and validation.",
1933
+ "description": "An international phone number input with country code selection, format-as-you-type, and E.164 validation.",
1934
1934
  "category": "form",
1935
1935
  "dependencies": [
1936
1936
  "@loworbitstudio/visor-core",
1937
- "intl-tel-input"
1937
+ "intl-tel-input",
1938
+ "@intl-tel-input/react"
1938
1939
  ],
1939
1940
  "registryDependencies": [
1940
1941
  "utils"
@@ -1943,12 +1944,12 @@
1943
1944
  {
1944
1945
  "path": "components/ui/phone-input/phone-input.tsx",
1945
1946
  "type": "registry:ui",
1946
- "content": "\"use client\"\n\nimport * as React from \"react\"\nimport \"intl-tel-input/styles\"\nimport { cn } from \"../../../lib/utils\"\n\nexport interface PhoneInputProps {\n /** HTML id attribute */\n id?: string\n /** HTML name attribute */\n name?: string\n /** Initial phone number value (e.g. \"+14155551234\") */\n value?: string\n /** Placeholder text */\n placeholder?: string\n /** Whether the field is required */\n required?: boolean\n /** Whether the field is disabled */\n disabled?: boolean\n /** Size variant — matches Input component sizes */\n size?: \"sm\" | \"md\" | \"lg\"\n /** Additional CSS classes applied to the wrapper */\n className?: string\n /** Called with (fullInternationalNumber, isValid) on change */\n onChange?: (value: string, isValid: boolean) => void\n /** Called on blur */\n onBlur?: () => void\n}\n\ninterface ItiInstance {\n getNumber: () => string\n isValidNumber: () => boolean | null\n destroy: () => void\n}\n\nconst PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(\n (\n {\n id,\n name,\n value = \"\",\n placeholder,\n required = false,\n disabled = false,\n size = \"md\",\n className,\n onChange,\n onBlur,\n },\n ref\n ) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const itiRef = React.useRef<ItiInstance | null>(null)\n const initializedRef = React.useRef(false)\n const [isMounted, setIsMounted] = React.useState(false)\n\n // CRITICAL: Store callbacks in refs to prevent re-initialization on parent re-render.\n // Without this, new onChange reference → handleChange recreates → useEffect re-runs\n // → intl-tel-input destroys/recreates → input loses focus.\n // See docs/wisdom/W007-intl-tel-input-focus.md\n const onChangeRef = React.useRef(onChange)\n const onBlurRef = React.useRef(onBlur)\n\n React.useEffect(() => {\n onChangeRef.current = onChange\n }, [onChange])\n\n React.useEffect(() => {\n onBlurRef.current = onBlur\n }, [onBlur])\n\n React.useEffect(() => {\n setIsMounted(true)\n }, [])\n\n // Merge forwarded ref with internal ref\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n ;(inputRef as React.MutableRefObject<HTMLInputElement | null>).current =\n node\n if (typeof ref === \"function\") {\n ref(node)\n } else if (ref) {\n ;(ref as React.MutableRefObject<HTMLInputElement | null>).current =\n node\n }\n },\n [ref]\n )\n\n // Stable handler — reads from refs, never recreates\n const handleChange = React.useCallback(() => {\n if (!itiRef.current) return\n\n try {\n const number = itiRef.current.getNumber()\n const isValid = itiRef.current.isValidNumber() ?? false\n onChangeRef.current?.(number, isValid)\n } catch {\n const input = inputRef.current\n if (input) {\n onChangeRef.current?.(input.value, input.value.length > 0)\n }\n }\n }, [])\n\n React.useEffect(() => {\n if (!isMounted || !inputRef.current || initializedRef.current) return\n\n let cleanup: (() => void) | undefined\n\n const initializeIntlTelInput = async () => {\n const intlTelInputModule = await import(\n \"intl-tel-input/intlTelInputWithUtils\"\n )\n const intlTelInput = intlTelInputModule.default\n\n const input = inputRef.current\n if (!input) return\n\n const iti = intlTelInput(input, {\n initialCountry: \"auto\",\n geoIpLookup: (callback, failure) => {\n fetch(\"https://ipapi.co/country/\", {\n headers: { Accept: \"text/plain\" },\n })\n .then((res) => res.text())\n .then((country) =>\n callback(country.trim().toLowerCase() as \"us\")\n )\n .catch(() => failure())\n },\n separateDialCode: true,\n formatAsYouType: true,\n formatOnDisplay: true,\n strictMode: true,\n countrySearch: true,\n dropdownContainer: document.body,\n })\n\n itiRef.current = iti\n initializedRef.current = true\n\n input.addEventListener(\"countrychange\", handleChange)\n input.addEventListener(\"input\", handleChange)\n\n cleanup = () => {\n input.removeEventListener(\"countrychange\", handleChange)\n input.removeEventListener(\"input\", handleChange)\n if (itiRef.current) {\n try {\n itiRef.current.destroy()\n } catch {\n // Ignore errors during cleanup\n }\n itiRef.current = null\n }\n initializedRef.current = false\n }\n }\n\n initializeIntlTelInput()\n\n return () => {\n cleanup?.()\n }\n }, [isMounted, handleChange])\n\n const handleBlur = () => {\n handleChange()\n onBlurRef.current?.()\n }\n\n // SSR-safe: render basic input during server render\n if (!isMounted) {\n return (\n <div data-slot=\"phone-input\" data-size={size} className={cn(className)}>\n <input\n type=\"tel\"\n id={id}\n name={name}\n defaultValue={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n autoComplete=\"tel\"\n data-1p-ignore\n data-lpignore=\"true\"\n data-bwignore\n data-form-type=\"other\"\n />\n </div>\n )\n }\n\n return (\n <div data-slot=\"phone-input\" data-size={size} className={cn(className)}>\n <input\n ref={setRefs}\n type=\"tel\"\n id={id}\n name={name}\n defaultValue={value}\n placeholder={placeholder}\n required={required}\n disabled={disabled}\n autoComplete=\"tel\"\n data-1p-ignore\n data-lpignore=\"true\"\n data-bwignore\n data-form-type=\"other\"\n onBlur={handleBlur}\n />\n </div>\n )\n }\n)\nPhoneInput.displayName = \"PhoneInput\"\n\nexport { PhoneInput }\n"
1947
+ "content": "\"use client\"\n\nimport * as React from \"react\"\nimport IntlTelInput from \"@intl-tel-input/react\"\nimport type { IntlTelInputRef } from \"@intl-tel-input/react\"\nimport type { CountrySelectorMode, Iso2, ValidationError } from \"intl-tel-input\"\nimport \"intl-tel-input/styles\"\nimport { cn } from \"../../../lib/utils\"\n\n// Password managers mangle tel fields. Typed through `input` props because the\n// library's `inputProps` surface does not model arbitrary data attributes.\nconst PASSWORD_MANAGER_OPT_OUT = {\n \"data-1p-ignore\": true,\n \"data-lpignore\": \"true\",\n \"data-bwignore\": true,\n \"data-form-type\": \"other\",\n} as React.ComponentPropsWithoutRef<\"input\">\n\nexport interface PhoneInputPortal {\n /** Element the country selector is portaled into. */\n container: HTMLElement\n /**\n * Theme-scoping class applied to the portaled country selector. Required —\n * a selector portaled out of the tree escapes the consumer's theme class,\n * so the scope has to travel with it.\n */\n scopeClassName: string\n}\n\nexport interface PhoneInputProps {\n /** HTML id attribute */\n id?: string\n /** HTML name attribute */\n name?: string\n /** Phone number in E.164 (e.g. \"+14155551234\"). Displayed formatted for the detected country. */\n value?: string | null\n /** Placeholder text */\n placeholder?: string\n /** Whether the field is required */\n required?: boolean\n /** Whether the field is disabled */\n disabled?: boolean\n /** Whether the field is read-only. Pair with `countrySelectorMode=\"OFF\"` to also close off the country dropdown. */\n readOnly?: boolean\n /** Country selector behaviour. `\"OFF\"` renders the selected country as a non-interactive element. */\n countrySelectorMode?: CountrySelectorMode\n /** ISO 3166-1 alpha-2 country selected on mount. */\n initialCountry?: Iso2\n /** Size variant — matches Input component sizes */\n size?: \"sm\" | \"md\" | \"lg\"\n /** Additional CSS classes applied to the wrapper */\n className?: string\n /**\n * Portal the country selector out of the input's DOM position. All-or-nothing:\n * the scoping class is not separable from the container, so the selector can\n * never escape its theme. Omit for the default inline selector.\n */\n portal?: PhoneInputPortal\n /**\n * Called with the E.164 number and its validity. `e164` is `null` for anything\n * that is not a complete, valid number — a partial is never emitted.\n */\n onChange?: (e164: string | null, isValid: boolean) => void\n /** Called on blur with the validation error for a non-empty, invalid number (otherwise `null`). */\n onBlur?: (error: ValidationError | null) => void\n}\n\nconst PhoneInput = React.forwardRef<IntlTelInputRef, PhoneInputProps>(\n (\n {\n id,\n name,\n value,\n placeholder,\n required = false,\n disabled = false,\n readOnly = false,\n countrySelectorMode,\n initialCountry = \"us\",\n size = \"md\",\n className,\n portal,\n onChange,\n onBlur,\n },\n ref\n ) => {\n const itiRef = React.useRef<IntlTelInputRef>(null)\n // `undefined` until the first emission — an initial `{ null, false }` would be\n // indistinguishable from the first partial the user types and would swallow it.\n const lastEmitted = React.useRef<\n { e164: string | null; isValid: boolean } | undefined\n >(undefined)\n\n React.useImperativeHandle(ref, () => ({\n getInstance: () => itiRef.current?.getInstance() ?? null,\n getInput: () => itiRef.current?.getInput() ?? null,\n }))\n\n // `emit` must be identity-stable. The wrapper derives its internal `update`\n // from the onChange* props and keys its value-writeback effect on\n // `[value, update]`; a new `emit` each render re-runs that effect, and while\n // the field is unfocused and holding a partial (so the controlled value is\n // null) it calls setNumber(\"\") and erases what the user typed. Reading the\n // consumer's callback from a ref keeps the effect keyed on `value` alone.\n const onChangeRef = React.useRef(onChange)\n onChangeRef.current = onChange\n\n // The library's own `onChangeNumber` emits `getNumber()` verbatim, which for a\n // partial is a `+<dial code>` prefix glued to the formatted national string\n // (typing \"21337\" yields \"+1213-37\"). Derive the value at emit time instead so\n // a malformed number can never reach the consumer.\n const emit = React.useCallback(() => {\n const iti = itiRef.current?.getInstance()\n const isValid = iti?.isValidNumber() ?? false\n const e164 = isValid ? iti!.getNumber() : null\n const last = lastEmitted.current\n if (last && last.e164 === e164 && last.isValid === isValid) return\n lastEmitted.current = { e164, isValid }\n onChangeRef.current?.(e164, isValid)\n }, [])\n\n const handleBlur = React.useCallback(() => {\n const iti = itiRef.current?.getInstance()\n const input = itiRef.current?.getInput()\n // The error callback never fires while a number is still partial, so read the\n // error directly — a visibly-present partial that saves as nothing is silent\n // data loss. Gate on validity: getValidationError() reports \"IS_POSSIBLE\" for\n // a number that is fine, which is not an error.\n const error =\n iti && input?.value && !iti.isValidNumber()\n ? (iti.getValidationError() ?? null)\n : null\n onBlur?.(error)\n }, [onBlur])\n\n return (\n <div data-slot=\"phone-input\" data-size={size} className={cn(className)}>\n <IntlTelInput\n ref={itiRef}\n value={value}\n disabled={disabled}\n readOnly={readOnly}\n onChangeNumber={emit}\n onChangeCountry={emit}\n onChangeValidity={emit}\n initialCountry={initialCountry}\n initialCountryLookup={null}\n countrySelectorMode={countrySelectorMode}\n separateDialCode\n formatAsYouType\n strictMode\n countrySearch\n loadUtils={() => import(\"intl-tel-input/utils\")}\n dropdownParent={portal?.container ?? null}\n classNames={\n portal\n ? { countrySelectorContainer: portal.scopeClassName }\n : undefined\n }\n inputProps={{\n id,\n name,\n placeholder,\n required,\n autoComplete: \"tel\",\n onBlur: handleBlur,\n ...PASSWORD_MANAGER_OPT_OUT,\n }}\n />\n </div>\n )\n }\n)\nPhoneInput.displayName = \"PhoneInput\"\n\nexport { PhoneInput }\n"
1947
1948
  },
1948
1949
  {
1949
1950
  "path": "components/ui/phone-input/phone-input.css",
1950
1951
  "type": "registry:ui",
1951
- "content": "/* intl-tel-input theme overrides — Visor design tokens\n *\n * Global (not CSS Module) because intl-tel-input renders DOM outside React\n * and appends the dropdown to document.body. All overrides need !important\n * to beat the library's shipped styles loaded via dynamic import.\n *\n * Token mapping from Blacklight --theme-* → Visor semantic tokens:\n * --theme-background → --surface-interactive-default (inputs)\n * --theme-muted → --surface-card (dropdown surfaces)\n * --theme-border → --border-default\n * --theme-foreground → --text-primary\n * --theme-muted-foreground → --text-secondary\n * --theme-accent → --interactive-primary-bg (selected state)\n * --theme-accent-foreground → --text-inverse (text on accent)\n */\n\n/* Container fills available width */\n.iti {\n display: block !important;\n width: 100% !important;\n}\n\n/* The input element itself — match Visor Input component (input.module.css) */\n.iti input.iti__tel-input {\n width: 100% !important;\n min-width: 0 !important;\n border: 1px solid var(--border-default, #e5e7eb) !important;\n background-color: var(--surface-interactive-default, #f9fafb) !important;\n /* padding-left is set dynamically by intl-tel-input to fit the flag + dial code */\n color: var(--text-primary, #111827) !important;\n line-height: 1.5 !important;\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out) !important;\n outline: none !important;\n}\n\n/* Size: sm — compact (default for backwards compat) */\n[data-size=\"sm\"] .iti input.iti__tel-input {\n height: 2.25rem !important;\n padding-top: var(--spacing-1, 0.25rem) !important;\n padding-bottom: var(--spacing-1, 0.25rem) !important;\n padding-right: var(--spacing-3, 0.75rem) !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n border-radius: var(--radius-sm, 0.25rem) !important;\n}\n\n/* Size: md (default) */\n[data-size=\"md\"] .iti input.iti__tel-input {\n height: auto !important;\n padding-top: 0.875rem !important;\n padding-bottom: 0.875rem !important;\n padding-right: 1rem !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n border-radius: var(--radius-sm, 0.5rem) !important;\n}\n\n/* Size: lg */\n[data-size=\"lg\"] .iti input.iti__tel-input {\n height: auto !important;\n padding-top: 1.125rem !important;\n padding-bottom: 1.125rem !important;\n padding-right: 1.25rem !important;\n font-size: var(--font-size-base, 1rem) !important;\n border-radius: var(--radius-sm, 0.5rem) !important;\n}\n\n.iti input.iti__tel-input::placeholder {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n.iti input.iti__tel-input:focus {\n border-color: var(--border-focus, #111827) !important;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent) !important;\n}\n\n.iti input.iti__tel-input:disabled {\n pointer-events: none !important;\n cursor: not-allowed !important;\n opacity: 0.5 !important;\n}\n\n/* Dropdown content (search + country list container) — must be opaque since\n intl-tel-input renders on document.body, potentially outside theme wrappers */\n.iti__dropdown-content {\n background-color: var(--surface-card, #ffffff) !important;\n border: 1px solid var(--border-default, #e5e7eb) !important;\n border-radius: var(--radius-lg, 0.5rem) !important;\n box-shadow: var(--shadow-md) !important;\n z-index: var(--z-index-dropdown, 1000) !important;\n min-width: 18rem !important;\n}\n\n/* Search input inside dropdown — match Visor Input base */\n.iti__search-input {\n background-color: var(--surface-interactive-default, #f9fafb) !important;\n border: 1px solid var(--border-default, #e5e7eb) !important;\n color: var(--text-primary, #111827) !important;\n border-radius: var(--radius-md, 0.375rem) !important;\n padding: var(--spacing-2, 0.5rem) var(--spacing-2, 0.5rem) var(--spacing-2, 0.5rem) 2rem !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n outline: none !important;\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out) !important;\n}\n\n.iti__search-input:focus {\n border-color: var(--border-focus, #111827) !important;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent) !important;\n}\n\n.iti__search-input::placeholder {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n/* Divider between search and country list */\n.iti__search-input-wrapper {\n border-bottom: 1px solid var(--border-default, #e5e7eb) !important;\n}\n\n/* Country list background — matches dropdown content */\n.iti__country-list {\n background-color: var(--surface-card, #ffffff) !important;\n}\n\n/* Individual country items — match Select .item */\n.iti__country {\n color: var(--text-primary, #111827) !important;\n padding: var(--spacing-2, 0.5rem) var(--spacing-3, 0.75rem) !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n line-height: 1.4 !important;\n white-space: nowrap !important;\n gap: var(--spacing-2, 0.5rem) !important;\n}\n\n/* Hover and keyboard highlight — match Select .item:focus */\n.iti__country:hover,\n.iti__country.iti__highlight {\n background-color: var(--surface-interactive-hover, #f3f4f6) !important;\n}\n\n/* Selected/active country — match interactive primary */\n.iti__country.iti__active {\n background-color: var(--interactive-primary-bg, var(--primary, #111827)) !important;\n color: var(--text-inverse, #ffffff) !important;\n}\n\n/* Dial code in country list */\n.iti__dial-code {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n/* Inherit color for active state (overrides dial-code muted color) */\n.iti__country-name,\n.iti__country.iti__active .iti__country-name,\n.iti__country.iti__active .iti__dial-code {\n color: inherit !important;\n}\n\n/* Selected country button (dropdown trigger in the input) */\n.iti__selected-country {\n background-color: transparent !important;\n border-radius: var(--radius-md, 0.375rem) 0 0 var(--radius-md, 0.375rem) !important;\n transition: background-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out) !important;\n}\n\n.iti__selected-country:hover,\n.iti__selected-country:focus {\n background-color: var(--surface-interactive-hover, #f3f4f6) !important;\n}\n\n/* Arrow/chevron */\n.iti__arrow {\n border-top-color: var(--text-secondary, #9ca3af) !important;\n}\n\n.iti__arrow--up {\n border-bottom-color: var(--text-secondary, #9ca3af) !important;\n}\n\n/* Selected dial code text */\n.iti__selected-dial-code {\n color: var(--text-primary, #111827) !important;\n margin-right: var(--spacing-2, 0.5rem) !important;\n}\n"
1952
+ "content": "/* intl-tel-input v29 theming — Visor design tokens\n *\n * Global (not a CSS Module) because intl-tel-input renders its own DOM, and the\n * country selector can be portaled out of the React tree.\n *\n * v29 exposes CSS custom properties for the country selector, so the dropdown is\n * themed by binding those rather than by overriding rules. Every binding falls\n * back to the vendor default, so a theme missing a token behaves exactly as the\n * unstyled library does. The input chrome still needs explicit rules — the\n * library ships no colours for the `<input>` itself.\n */\n\n.iti {\n display: block;\n width: 100%;\n\n /* v29 country-selector variables → Visor tokens (vendor defaults as fallbacks) */\n --iti-country-selector-bg: var(--surface-popover, white);\n --iti-border-color: var(--border-default, #ccc);\n --iti-icon-color: var(--text-secondary, #555);\n --iti-hover-color: var(--surface-interactive-hover, rgba(0, 0, 0, 0.05));\n --iti-strict-reject-flash-color: var(\n --surface-error-subtle,\n rgba(255, 0, 0, 0.12)\n );\n\n /* The country list sets no colour of its own — it inherits. Without this the\n list would take its colour from whatever encloses the input. */\n color: var(--text-primary, #111827);\n}\n\n/* The input element itself — match Visor Input component (input.module.css) */\n.iti input.iti__tel-input {\n width: 100% !important;\n min-width: 0 !important;\n border: var(--stroke-width-thin, 1px) solid var(--border-default, #e5e7eb) !important;\n background-color: var(--surface-interactive-default, #f9fafb) !important;\n /* padding-left is set dynamically by intl-tel-input to fit the flag + dial code */\n color: var(--text-primary, #111827) !important;\n line-height: 1.5 !important;\n transition: border-color var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out),\n box-shadow var(--motion-duration-150, 150ms) var(--motion-easing-default, ease-in-out) !important;\n outline: none !important;\n}\n\n/* Size: sm — compact */\n[data-size=\"sm\"] .iti input.iti__tel-input {\n height: 2.25rem !important;\n padding-top: var(--spacing-1, 0.25rem) !important;\n padding-bottom: var(--spacing-1, 0.25rem) !important;\n padding-right: var(--spacing-3, 0.75rem) !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n border-radius: var(--radius-sm, 0.25rem) !important;\n}\n\n/* Size: md (default) */\n[data-size=\"md\"] .iti input.iti__tel-input {\n height: auto !important;\n padding-top: 0.875rem !important;\n padding-bottom: 0.875rem !important;\n padding-right: 1rem !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n border-radius: var(--radius-sm, 0.5rem) !important;\n}\n\n/* Size: lg */\n[data-size=\"lg\"] .iti input.iti__tel-input {\n height: auto !important;\n padding-top: 1.125rem !important;\n padding-bottom: 1.125rem !important;\n padding-right: 1.25rem !important;\n font-size: var(--font-size-base, 1rem) !important;\n border-radius: var(--radius-sm, 0.5rem) !important;\n}\n\n.iti input.iti__tel-input::placeholder {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n.iti input.iti__tel-input:focus {\n border-color: var(--border-focus, #111827) !important;\n box-shadow: 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent) !important;\n}\n\n.iti input.iti__tel-input:disabled {\n pointer-events: none !important;\n cursor: not-allowed !important;\n opacity: var(--opacity-50, 0.5) !important;\n}\n\n/* Country selector panel — v29 hardcodes a 3px radius and an untokenised shadow */\n.iti__country-selector {\n border-radius: var(--radius-md, 0.375rem) !important;\n box-shadow: var(--shadow-md) !important;\n}\n\n/* With countrySearch on, v29 pins the panel height so it does not jump while the\n list is filtered — but it measures that height in a throwaway container on\n document.body, outside this page's typography. The search field's line-height\n is inherited, so it measured shorter there than it renders here and the pinned\n height came up short, letting the square-cornered list and its scrollbar spill\n past the panel's rounded bottom corner. Clipping is the guard; pinning\n line-height on the search field is the fix — it makes the measurement and the\n render agree, so nothing needs to be clipped in the first place. */\n.iti--inline-country-selector .iti__country-selector {\n overflow: hidden !important;\n}\n\n/* The search field is an <input>: it takes UA `Field`/`FieldText` colours rather\n than inheriting, so on a dark theme it would render as a white box inside a\n dark panel. The library ships no colours for it. */\n.iti__search-input {\n background-color: transparent !important;\n color: var(--text-primary, #111827) !important;\n font-size: var(--font-size-sm, 0.875rem) !important;\n /* Must be an explicit value, not inherited — see the panel-clipping note above.\n An inherited line-height resolves differently in v29's off-page measuring\n container than it does here, which throws off the pinned panel height. */\n line-height: 1.5 !important;\n outline: none !important;\n}\n\n.iti__search-input::placeholder {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n.iti__search-input:focus-visible {\n box-shadow: inset 0 0 0 var(--focus-ring-width, 2px) color-mix(in srgb, var(--border-focus, #111827) 15%, transparent) !important;\n}\n\n/* Dial code beside the country name in the list */\n.iti__dial-code {\n color: var(--text-secondary, #9ca3af) !important;\n}\n\n/* Selected country trigger overlaying the left of the input */\n.iti__selected-country-primary {\n border-radius: var(--radius-md, 0.375rem) 0 0 var(--radius-md, 0.375rem) !important;\n}\n\n.iti__selected-dial-code {\n color: var(--text-primary, #111827) !important;\n}\n"
1952
1953
  },
1953
1954
  {
1954
1955
  "path": "components/ui/phone-input/phone-input.module.css",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.4.0",
3
- "generated_at": "2026-09-05T02:55:23.174Z",
3
+ "generated_at": "2026-09-09T19:18:41.930Z",
4
4
  "components": {
5
5
  "accessibility-specimen": {
6
6
  "category": "specimen",
@@ -6623,7 +6623,7 @@
6623
6623
  },
6624
6624
  "phone-input": {
6625
6625
  "category": "form",
6626
- "description": "An international phone number input with country code selection, geo-IP detection, format-as-you-type, and validation.",
6626
+ "description": "An international phone number input with country code selection, format-as-you-type, and E.164 validation.",
6627
6627
  "when_to_use": [
6628
6628
  "Phone number entry fields (domestic or international)",
6629
6629
  "Contact forms requiring phone numbers",
@@ -6646,8 +6646,8 @@
6646
6646
  },
6647
6647
  {
6648
6648
  "name": "value",
6649
- "type": "string",
6650
- "description": "Initial phone number value (e.g. \"+14155551234\")."
6649
+ "type": "string | null",
6650
+ "description": "Phone number in E.164 (e.g. \"+14155551234\"). Displayed formatted for the detected country."
6651
6651
  },
6652
6652
  {
6653
6653
  "name": "placeholder",
@@ -6657,13 +6657,33 @@
6657
6657
  {
6658
6658
  "name": "required",
6659
6659
  "type": "boolean",
6660
+ "default": "false",
6660
6661
  "description": "Whether the field is required."
6661
6662
  },
6662
6663
  {
6663
6664
  "name": "disabled",
6664
6665
  "type": "boolean",
6666
+ "default": "false",
6665
6667
  "description": "Whether the field is disabled."
6666
6668
  },
6669
+ {
6670
+ "name": "readOnly",
6671
+ "type": "boolean",
6672
+ "default": "false",
6673
+ "description": "Whether the field is read-only. Pair with countrySelectorMode=\"OFF\" to also close off the country dropdown."
6674
+ },
6675
+ {
6676
+ "name": "countrySelectorMode",
6677
+ "type": "\"OFF\" | \"DROPDOWN\" | \"FULLSCREEN\" | \"AUTO\"",
6678
+ "default": "\"AUTO\"",
6679
+ "description": "Country selector behaviour. \"OFF\" renders the selected country as a non-interactive element."
6680
+ },
6681
+ {
6682
+ "name": "initialCountry",
6683
+ "type": "Iso2",
6684
+ "default": "\"us\"",
6685
+ "description": "ISO 3166-1 alpha-2 country selected on mount."
6686
+ },
6667
6687
  {
6668
6688
  "name": "size",
6669
6689
  "type": "\"sm\" | \"md\" | \"lg\"",
@@ -6675,22 +6695,28 @@
6675
6695
  "type": "string",
6676
6696
  "description": "Additional CSS classes applied to the wrapper."
6677
6697
  },
6698
+ {
6699
+ "name": "portal",
6700
+ "type": "{ container: HTMLElement; scopeClassName: string }",
6701
+ "description": "Portal the country selector out of the input's DOM position. All-or-nothing — the theme-scoping class is not separable from the container, so a portaled selector can never escape its theme. Omit for the default inline selector.\n"
6702
+ },
6678
6703
  {
6679
6704
  "name": "onChange",
6680
- "type": "(value: string, isValid: boolean) => void",
6681
- "description": "Called with the full international number and validation status on every change."
6705
+ "type": "(e164: string | null, isValid: boolean) => void",
6706
+ "description": "Called with the E.164 number and its validity. e164 is null for anything that is not a complete, valid number — a partial is never emitted."
6682
6707
  },
6683
6708
  {
6684
6709
  "name": "onBlur",
6685
- "type": "() => void",
6686
- "description": "Called when the input loses focus."
6710
+ "type": "(error: ValidationError | null) => void",
6711
+ "description": "Called on blur with the validation error for a non-empty, invalid number (otherwise null)."
6687
6712
  }
6688
6713
  ],
6689
6714
  "dependencies": [
6690
- "intl-tel-input"
6715
+ "intl-tel-input",
6716
+ "@intl-tel-input/react"
6691
6717
  ],
6692
6718
  "tokens_used": [],
6693
- "example": "<PhoneInput\n name=\"phone\"\n placeholder=\"Enter phone number\"\n onChange={(value, isValid) => {\n console.log(value, isValid);\n }}\n/>\n"
6719
+ "example": "<PhoneInput\n name=\"phone\"\n placeholder=\"Enter phone number\"\n onChange={(e164, isValid) => {\n console.log(e164, isValid);\n }}\n/>\n"
6694
6720
  },
6695
6721
  "popover": {
6696
6722
  "category": "feedback",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loworbitstudio/visor",
3
- "version": "1.22.0",
3
+ "version": "1.24.0",
4
4
  "description": "CLI for the Visor design system — add components, hooks, and utilities to your project.",
5
5
  "type": "module",
6
6
  "bin": {