arkynChangelogGuides
docs / changelogs / migration-v2-to-v3

Arkyn v2.2.3 → v3.0.2 Migration Guide

This guide compares the public API of the Arkyn packages as they exist on compare-branch (v2.2.3) against develop (v3.0.2, the first stable release of the v3.x.x line, released 2026-07-13). Every item below was verified directly against source code in both refs, not against commit history.
This is the deep, side-by-side reference for the whole v2.2.3 → v3.0.2 jump, including changes made in betas before v3.0.1-beta.139, which predate this project's per-version changelog and therefore don't appear in Breaking Changes, the v3.0.1-beta changelog, or the v3.x.x changelog. If you're already on v3.0.2 or later and just want to know what changed release-to-release, those pages are a better fit than this one.

High-level summary

v3.0.2 is a near-total rewrite of the Arkyn monorepo's build and API surface, on top of many individual component changes:
  • Framework decoupling. Every @remix-run/* peer dependency is dropped across components and server. Hooks/utilities that used to read Remix's useActionData/useFetchers/useLocation/useNavigate directly (useFieldErrors, useAutomation, useScopedParams, Pagination) now take plain data/strings as arguments and leave routing/data-fetching to the consumer.
  • @arkyn/types is gone. All prop/context types (previously imported from the standalone @arkyn/types package) are now declared inline in each component/hook/provider file and are largely not re-exported by name. See Removed: @arkyn/types.
  • ESM-only, unbundled, tree-shakeable output. Every package drops its single dist/bundle.js in favor of unbundled per-module ESM output plus a granular exports map (subpath imports like @arkyn/components/button, @arkyn/shared/formatToCep, etc.). See Tooling / packaging changes.
  • New form-field architecture. The Remix-coupled Form/FormController/FormLabel/FormError components and useFieldErrors() hook are replaced by a generic FormProvider + useForm() + FieldWrapper/FieldLabel/FieldError trio, used consistently across Input, Select, MultiSelect, Textarea, RadioGroup, RichText, and the upload components.
  • @arkyn/newcomponents (an experimental v2.1.0 package) is folded into @arkyn/components. Its Badge/Button implementation (not the old @arkyn/components one) is what shipped in v3.
  • Google Maps → Mapbox. GoogleMap is replaced by MapView, built on mapbox-gl instead of @react-google-maps/api, with a different props/marker shape and a Mapbox access token instead of a Google Maps API key.
  • Several components removed with no replacement: Breadcrumb, Card, Skeleton.
  • Several components are new: AudioPlayer, Calendar, FullCalendar, CardTabButton/CardTabContainer, CurrencyInput, MaskedInput, FieldError/FieldLabel/FieldWrapper, GoogleAnalytics.
  • @arkyn/server gains an HTTP client (ApiService), logging services, and absorbs all validators from @arkyn/shared, plus a new validateEmail. Zod moves from a bundled v3 dependency to a peer dependency on Zod v4, a breaking major-version bump for any schema/validator code.
  • @arkyn/shared drops the uuid dependency (native crypto.randomUUID), and several date/format functions changed default enum literals and rounding/timezone behavior.
  • @arkyn/templates's countries data dropped the prefix field from every entry and Brazil's mask changed from a single string to a two-entry array, the first breaking change to a widely-consumed data shape.

Package inventory changes

  • @arkyn/components (2.2.3) → @arkyn/components (3.0.2), continued
  • @arkyn/newcomponents (2.1.0, experimental) → removed, merged into @arkyn/components
  • @arkyn/server (2.2.3) → @arkyn/server (3.0.2), continued
  • @arkyn/shared (2.2.3) → @arkyn/shared (3.0.2), continued
  • @arkyn/templates (2.2.3) → @arkyn/templates (3.0.2), continued
  • @arkyn/types (2.2.3) → removed, no replacement package; types co-located per-file
  • (new)apps/development (new, private: true, internal playground app, not published)

@arkyn/components

Package metadata & build

  • main
    • v2.2.3: ./dist/bundle.js
    • v3.0.2: ./dist/index.js
  • module
    • v2.2.3: (none)
    • v3.0.2: ./dist/index.js
  • exports map
    • v2.2.3: (none, root import only)
    • v3.0.2: ~122 subpath entries: one JS+.d.ts pair per component/hook/provider/service, plus a <name>.css sibling for every component with styles, plus "./styles" for the full aggregate stylesheet
  • peerDependencies
    • v2.2.3: @remix-run/react ^2.15.0, react ^18.3.1, react-dom ^18.3.1
    • v3.0.2: react >=18.0.0, react-dom >=18.0.0, plus many new, mostly-optional peers: @react-google-maps/api, @react-input/mask, html-react-parser, is-hotkey, lucide-react, mapbox-gl, react-hot-toast, react-scroll, slate, slate-history, slate-react
  • dependencies
    • v2.2.3: bundled all of the above libraries directly
    • v3.0.2: only internal workspace deps: @arkyn/shared, @arkyn/templates
  • files
    • v2.2.3: controlled by root .npmignore
    • v3.0.2: explicit allowlist: ["dist", "README.md", "LICENSE.txt", "styles.d.ts"]
Action required: consumers must now npm install (or add to package.json) the specific peer libraries used by the components they actually import (e.g. mapbox-gl for MapView, slate/slate-react/slate-history for RichText, react-hot-toast for the toast system), these are no longer bundled transitively.
CSS import is now required. v2.2.3 had no consumer-facing stylesheet entry point documented. v3.0.2 requires either:

ts

import "@arkyn/components/styles"; // all components' CSS
or, to avoid pulling in unused CSS:

ts

import { Button } from "@arkyn/components/button";
import "@arkyn/components/button.css"; // just Button's CSS (+ anything it renders internally)
There is no CSS custom-property/token override API documented, theming is scoped to which stylesheet(s) you import, not variable overrides. Nearly every component's CSS custom properties gained fallback values (e.g. var(--border, #e2e8f0)), so components render with sensible defaults even without a host theme.

Barrel export changes (src/index.ts)

  • Import paths moved from PascalCase folders (./components/Alert) to camelCase (./components/alert), and most components lost their local barrel index.ts, every subcomponent is exported directly from the package root.
  • Renamed: GoogleProviderPlacesProvider; useFieldErrorsuseForm (behavior changed, see Providers); getHtmlFromRichTextValuetoHtml; getRichTextValueFromHtmltoRichTextValue.
  • Removed: Card, Skeleton, Breadcrumb family (BreadcrumbContainer, BreadcrumbLink), Form family (FormController, FormLabel, FormError, useFormController), Toast (the standalone visual component, was already dead code, see Toast), GoogleMap, GoogleSearchPlaces, isHtml, morpheme, GenerateIcon, generatePagesArray, formatCurrency, clearNumber, maskValues, normalizeValue (internal service helpers no longer exported).
  • New: AudioPlayer, Calendar, FullCalendar, CardTabButton, CardTabContainer, CurrencyInput, MaskedInput, FieldError, FieldLabel, FieldWrapper, FormProvider, GoogleAnalytics, MapView, SearchPlaces, useForm, useScrollLock, useSearchAutomation, useSlider.

Removed components (no replacement)

  • Breadcrumb (BreadcrumbContainer, BreadcrumbLink), Fully removed. No equivalent anywhere in v3.0.2.
  • Card, Plain <div> wrapper, fully removed. The new CardTabButton/CardTabContainer are an unrelated tabbed-selector UI, not a generic container replacement, use a plain <div> instead.
  • Skeleton, Shimmer-loading placeholder, fully removed with no equivalent.
  • CpfCnpjInput (internal Input type="cpf-cnpj" variant), Removed as a ready-made component. CPF/CNPJ formatting logic moved to @arkyn/shared (formatToCpf, formatToCnpj); consumers must now compose MaskedInput + these formatters manually.

Renamed / restructured components (same concept, breaking prop changes)

Alert (alertContainer, alertContent, alertDescription, alertIcon, alertTitle)

  • AlertContainer: prop schema renamed to scheme (breaking).
  • Logic unchanged otherwise.

AudioUpload

  • onUpload renamed to onChange.
  • Label/error rendering now uses shared FieldWrapper/FieldLabel/FieldError instead of local subcomponents; error source is now useForm() instead of the Remix-coupled useFieldErrors().
  • Playback UI extracted into a new standalone AudioPlayer component (src, disabled, onPlayAudio, onPauseAudio receiving { currentTime, totalTime, formattedCurrentTime, formattedTotalTime }).

Badge

  • Now the @arkyn/newcomponents implementation (see Removed: @arkyn/newcomponents), not the old @arkyn/components one.
  • scheme union gains "secondary".
  • Default size changed from "md" to "lg", breaking if you relied on the implicit default.
  • Children now wrapped in a <p> internally (was raw children).

Button

  • Now the @arkyn/newcomponents implementation.
  • scheme union gains "secondary". No other prop changes.

Divider

  • No changes, continued as-is (path/casing only).

Drawer (drawer, drawerHeader, drawerContext.tsx)

  • DrawerContainer: prop isVisibled renamed to isVisible (typo fix, breaking).
  • Animation swapped from framer-motion to plain CSS + onAnimationEnd (framer-motion dependency dropped for this component).
  • New internal useScrollLock(isVisible) locks body scroll while open.
  • Public useDrawer/DrawerProvider (multi-drawer registry) API is unchanged, same drawerIsOpen(key)/drawerData(key)/openDrawer(key, data?)/closeDrawer(key), just re-sourced from providers/drawerProvider.tsx. Note there's also a second, unrelated internal useDrawer exported from drawer/drawerContext.tsx (only used by DrawerHeader's close button), don't confuse the two.

FacebookPixel

  • No prop changes. FacebookPixelProps is now exported by name (one of the few components that still does this).

FileUpload

  • onUpload renamed to onChange. Same FieldWrapper/FieldLabel/FieldError + useForm() rework as AudioUpload.

Form → removed; replaced by FieldWrapper + FieldLabel + FieldError + FormProvider + useForm()

This is the single biggest architectural change in the component set.
  • Old: FormController auto-derived field name/id from a nested input ref and exposed { error, id, inputRef } via context; FormLabel/FormError read that context implicitly. Errors came from useFieldErrors(), which read Remix's useActionData()/useFetchers()/useNavigation() directly.
  • New: FormProvider({ children, fieldErrors?, form? }) holds fieldErrors explicitly passed in by the app (e.g. from useActionData() in a Remix app, or any other source). useForm() returns { fieldErrors }. FieldWrapper (orientation?), FieldLabel (showAsterisk?, requires explicit htmlFor), and FieldError (renders children or null) are plain, context-free primitives that every form-field component (Input, Select, MultiSelect, Textarea, RadioGroup, upload components, RichText) now composes itself.
  • Migration action: wrap forms in <FormProvider fieldErrors={actionData?.fieldErrors}>, remove any FormController/FormLabel/FormError usage, and stop relying on automatic Remix wiring.

GoogleMap → MapView (breaking third-party SDK swap)

  • Rebuilt on Mapbox GL (mapbox-gl) instead of @react-google-maps/api.
  • coordinates?: {lat,lng}coordinates?: Coordinate | Coordinate[] (Coordinate = {lat, lng, data?, popUp?}), now supports multiple markers with attached data and custom popups.
  • New required accessToken: string (Mapbox public token) replaces GoogleProvider/googleMapsApiKey.
  • New onMarkerClick?: (coordinate) => void.
  • draggable prop removed, markers are no longer draggable.
  • Flag this as the highest-risk change in the release: different SDK, different API key type, different marker/coordinate shape.

GoogleSearchPlaces → SearchPlaces

  • Still built on @react-google-maps/api's StandaloneSearchBox (not swapped to Mapbox).
  • onPlaceChanged payload field renames: districtneighborhood, ceppostalCode.
  • More resilient place-type matching (checks multiple Google Places type keys instead of one).
  • Provider GoogleProviderPlacesProvider: prop googleMapsApiKeyapiKey; children pattern changed to a render prop, children: (isLoaded: boolean) => ReactNode instead of plain ReactNode. Update every <GoogleProvider>{children}</GoogleProvider> call site.

GoogleTagManager

  • Unchanged. A new, independent GoogleAnalytics component was added alongside it (GA4 support: measurementId, showInDevMode?), not a rename or replacement of GTM.

IconButton

  • scheme union gains "secondary". No other changes.

ImageUpload

  • onUpload renamed to onChange, now (url: string) => void (no longer optional).
  • Gains className, unShowFieldTemplate, orientation props not present on AudioUpload/FileUpload (inconsistent prop parity across the three upload components, flagged for maintainers, not just consumers). Note the orientation JSDoc says default "horizontal" but the code defaults to "vertical", verify actual behavior before relying on the default.

Input / CurrencyInput / MaskedInput / PhoneInput

The old single Input component dispatched on a type prop ("currency"|"masked"|"cpf-cnpj"|...) to internal variants. In v3, each variant is now an independent top-level component.
  • Input: narrowed to plain text input only (no more type variant dispatch). sufix renamed to suffix. Adds label, errorMessage, showAsterisk, orientation, unShowFieldTemplate. New disabled-state behavior: shows the current value as placeholder text when disabled.
  • CurrencyInput (new standalone): locale is now a required, fixed union of 22 currency codes (was a free-form locale+currency string pair). value/defaultValue changed from string to number. onChangeValue renamed to onChange. onKeyPress callback removed.
  • MaskedInput (new standalone): replacement is now required (was optional). No type prop. Does not use the shared FieldTemplate pattern, composes FieldWrapper/FieldLabel/FieldError manually and has no orientation prop, unlike the other three (inconsistency to flag).
  • PhoneInput: folder/subcomponents lowercased. Adds label, errorMessage, showAsterisk, orientation, unShowFieldTemplate, and a new optional value prop (component can now be controlled, was uncontrolled-only). defaultCountry renamed to defaultCountryIso. Internal country/mask parsing now delegates to @arkyn/shared's findCountryMask/formatToPhone instead of local utilities; CountryType.mask is now string | string[]. Gains smart dropdown positioning and useScrollLock.
  • CpfCnpjInput: removed (see Removed components).

Modal (modalContainer, modalHeader, modalFooter, modalContext.tsx)

  • ModalContainer: prop isVisibled renamed to isVisible (breaking, same typo-fix pattern as Drawer).
  • Animation swapped from framer-motion to plain CSS + onAnimationEnd; adds useScrollLock(isVisible).
  • Context (makeInvisible) moved from a bare createContext in Container/index.tsx to a dedicated ModalProvider/useModal() pair in modalContext.tsx (internal, ModalHeader uses it, not the public multi-modal useModal hook from hooks/useModal.ts, which is unrelated and unchanged; same naming collision as Drawer).
  • ModalHeader/ModalFooter: no prop changes (showCloseButton, alignment: "left"|"center"|"right"|"between"|"around" all unchanged).
  • Public useModal/ModalProvider (multi-modal registry, hooks/useModal.ts + providers/modalProvider.tsx) API is unchanged: same overloads and same { modalIsOpen, modalData, openModal, closeModal, closeAll } shape.

MultiSelect

  • onSelect renamed to onChange, now correctly reflects the next-state array (old had a stale-closure bug).
  • isError prop removed, now derived internally from errorMessage/fieldErrors[name].
  • Adds id, showAsterisk, label, errorMessage, unShowFieldTemplate, orientation. Now uses useForm() + FieldTemplate instead of useFormController().
  • Dropdown gains auto-flip positioning and useScrollLock.

Pagination (breaking, router coupling removed)

  • Old component called Remix's useNavigate() directly and read page/per_page from the URL via scope/pageKey/perPageKey props, it performed navigation itself.
  • New component is a plain controlled component: currentPage is now required (no default), and a new onChange?: (page: number) => void callback replaces the built-in navigation. scope, pageKey, perPageKey props are removed.
  • Migration action: any code relying on Pagination auto-updating the URL must now pass currentPage explicitly and implement its own onChange handler (e.g. calling navigate/setSearchParams yourself).

Popover

  • No prop changes. framer-motion dependency dropped (was already a no-op fade, duration: 0) in favor of a plain visibility toggle. Adds useScrollLock(isOpen).

Radio (radioBox, radioGroup, radioContext.tsx)

  • RadioBox gains isError?: boolean (per-item error override, falls back to group state).
  • RadioGroup gains label, showAsterisk, errorMessage, disabled (new group-wide disable, previously only per-box), unShowFieldTemplate, orientation. Now uses useForm()/FieldTemplate instead of useFormController().
  • Fix: user-supplied onClick/onFocus on RadioBox are no longer silently clobbered by the internal handler (previously {...rest} spread after the hardcoded handlers wiped out user callbacks).

RichText

Largest single-component change set; core Slate editor logic (toggleBlock/toggleMark/isBlockActive/isMarkActive, hotkeys) is unchanged, only reorganized into shared services//utils//templates/ folders.
  • maxLimit default changed from 2000 to 10000, a consumer relying on the implicit 2000 cap will silently allow far more text now.
  • orientation default is "vertical"; gains label, showAsterisk, id, unShowFieldTemplate, baseErrorMessage. Error/label chrome now handled internally via the shared FieldTemplate, replacing manual FormController/FormLabel wrapping.
  • Ref handling changed: the hidden <input> no longer accepts/forwards an external ref (previously exposed via useFormController()). Anything relying on grabbing that ref externally will break.
  • The hidden input's value is now .slice(0, maxLimit) on the serialized JSON string, not the visible text length, a quirky behavior worth testing against your own content.
  • New: video embeds. videoConfig (RichTextInsertVideoProps) + a new InsertVideo toolbar button (YouTube URLs only, extracts ?v= param). New "video" element/mark type. Important asymmetry: the video toolbar button renders whenever not explicitly hidden (no videoConfig required, unlike the image button which requires imageConfig), add "video" to hiddenButtons if you don't want it. Also, toHtml serializes a video node to <iframe>, but toRichTextValue has no matching deserialize case, video content saved as HTML will not round-trip back into a video block.
  • getHtmlFromRichTextValue/getRichTextValueFromHtml renamed to toHtml/toRichTextValue (same signatures).
  • isHtml moved out of @arkyn/components entirely, into @arkyn/shared.
  • slate/slate-history/slate-react are now peer dependencies (>=0.100.0) instead of bundled dependencies, add them to your own package.json.

Select

  • onSelect renamed to onChange, now returns just the value string (deselect calls onChange("")).
  • onFocus is now no-arg (was FocusEvent-based).
  • searchCountryPlaceholder/searchPlaceholder prop removed, the internal search input no longer accepts a placeholder.
  • Gains id, showAsterisk, label, errorMessage, unShowFieldTemplate, orientation. Uses useForm()/FieldTemplate instead of useFormController().
  • No public type export, SelectProps is no longer importable (was import type { SelectProps } from "@arkyn/types").
  • Dropdown gains auto-flip positioning and useScrollLock.

Slider (+ new useSlider hook)

  • disableDrag renamed to disabled.
  • Full HTML div attribute passthrough (including className) now supported, wasn't before.
  • New useSlider(defaultValue?): [number, setValue] hook, clamped 0–100 state pair designed to pair directly with <Slider>.

Switch

  • Gains label, orientation (default "horizontalReverse"), unShowFieldTemplate, showAsterisk, errorMessage. No longer depends on an external FormController context for id/ref (self-sufficient via useId()/useRef).

Table

  • No prop or behavior changes beyond path/casing. TableBody's empty-detection is slightly hardened (filters falsy children before counting).

Tabs → tab (+ new CardTab)

  • TabContainer: defaultActive renamed to defaultValue, onClick renamed to onChange. Gains disabled (also on TabButton).
  • Animated sliding underline removed, replaced by a static CSS ::after bar with no transition between tabs.
  • New CardTabButton/CardTabContainer: structurally a near-duplicate of Tab (same props/context shape) but styled as a segmented "pill" control instead of an underlined tab bar, not a Tabs replacement, a separate visual variant.

Textarea (breaking, form-context rework)

  • isError prop removed, replaced by derived errorMessage/fieldErrors lookup.
  • Gains label, showAsterisk, errorMessage, explicit value/defaultValue typing (controlled + uncontrolled).
  • No longer requires being nested inside a FormController, self-contained via useForm() + useId(), wrapped in FieldWrapper/FieldLabel/FieldError internally.

Toast

  • The standalone visual Toast component is removed, but this is lower-risk than it sounds: in both v2.2.3 and v3.0.2 the actual rendering was always delegated to react-hot-toast's own <Toaster>/toast.success()/toast.error(); the custom Toast component/CSS was already dead code, exported but never wired into showToast. Deleting it has no runtime effect for anyone using useToast()/showToast() as documented.
  • useToast()/showToast({message, type: "success"|"danger"}) API is unchanged.
  • GoogleProvider/ToastContext-style separate context files were consolidated into single provider files (providers/toastProvider.tsx), matching the pattern used by Drawer/Modal.

Tooltip

  • No prop changes. Gains adaptive positioning (flips topbottom/leftright on viewport overflow via a ResizeObserver-like effect).
  • Behavior change: text is now rendered via dangerouslySetInnerHTML (supports inline HTML) instead of as a plain string child, sanitize any dynamic text values yourself if they include user input.

New components

  • AudioPlayer, Standalone playback widget extracted from AudioUpload. Props: src (required), disabled, onPlayAudio/onPauseAudio (receive {currentTime, totalTime, formattedCurrentTime, formattedTotalTime}).
  • Calendar, Value-bound date picker (type: "single" | "range" discriminated union), variant?: "basic"|"complete", controlled/uncontrolled value/defaultValue/onChange, plus independent viewValue/onChangeView for the displayed month.
  • FullCalendar, Multi-view (day/week/month) scheduling/agenda calendar with events (title, date range, scheme, onClick) and blockedTimestamps. Navigation-only, no bound "selected value".
  • CardTabButton / CardTabContainer, Segmented "pill" tab control, see Tabs.
  • CurrencyInput, See Input family.
  • MaskedInput, See Input family.
  • FieldError / FieldLabel / FieldWrapper, Generic, context-free form-field primitives used across the library, see Form.
  • GoogleAnalytics, GA4 script injector, sibling to GoogleTagManager. Props: measurementId (required), showInDevMode?.

Hooks

  • useAutomation, Signature changed from no-args (reading Remix hooks internally) to useAutomation(formResponseData: any), caller now passes the raw server response instead of the hook reading it via Remix.
  • useDrawer, Unchanged API; re-sourced from providers/drawerProvider.
  • useModal, Unchanged API; re-sourced from providers/modalProvider.
  • useHydrated, Unchanged.
  • useScopedParams, Signature changed: useScopedParams(searchString: string, scope?: string), caller must now pass location.search explicitly (was read internally via Remix's useLocation()).
  • useToast, Unchanged.
  • useFieldErrors, Removed. Replaced by useForm() (see Form section), no longer reads Remix hooks automatically; fieldErrors must be passed into FormProvider explicitly.
  • useForm (new), useForm(): { fieldErrors }, reads FormProvider context.
  • useScrollLock (new), useScrollLock(isLocked: boolean): void, locks/restores document.body scroll. Used internally by Modal/Drawer/Popover/Select/MultiSelect dropdowns; also exported for custom overlays.
  • useSearchAutomation (new), useSearchAutomation(searchString: string, scope?: string), URL-param-driven variant of useAutomation.
  • useSlider (new), See Slider.

Providers

  • DrawerProvider, Unchanged behavior/shape, moved provider/providers/.
  • ModalProvider, Unchanged behavior/shape.
  • ToastProvider, Unchanged behavior/shape.
  • GoogleProviderPlacesProvider, Breaking: prop googleMapsApiKeyapiKey; children changed from plain ReactNode to a render-prop (isLoaded: boolean) => ReactNode.
  • FormProvider (new), { children, fieldErrors?, form? }, see Form.

Services (public)

Only two services remain publicly exported: toHtml and toRichTextValue (renamed from getHtmlFromRichTextValue/getRichTextValueFromHtml, same signatures). Everything else previously exported (isHtml, morpheme, GenerateIcon, generatePagesArray, formatCurrency, clearNumber, maskValues, normalizeValue) is now private implementation detail with no public replacement, if you imported these directly from @arkyn/components, you'll need to reimplement or find the equivalent in @arkyn/shared (several of these formatting concerns now live there).

@arkyn/server

Package metadata

  • main
    • v2.2.3: ./dist/bundle.js
    • v3.0.2: ./dist/index.js
  • exports map
    • v2.2.3: none
    • v3.0.2: ~34 subpath entries
  • dependencies
    • v2.2.3: @arkyn/shared, @aws-sdk/client-s3 ^3.782.0, sharp ^0.33.5, zod ^3.24.2
    • v3.0.2: @arkyn/shared, @arkyn/templates only
  • peerDependencies
    • v2.2.3: @remix-run/node ^2.16.4
    • v3.0.2: libphonenumber-js >=1.13.7, zod >=4.4.3
Breaking: @aws-sdk/client-s3 and sharp are no longer dependencies at all (check whether your code relied on them being transitively available). zod is now a required peer dependency on major version 4 (was bundled v3), this affects every schema passed to SchemaValidator/formParse/formAsyncParse, since Zod v4 renamed error.errors to error.issues and the Schema/SafeParseReturnType type names changed to ZodType/ZodSafeParseResult.

Removed exports

  • ApiInstance (GET/POST/PUT/PATCH/DELETE, uppercase methods), conceptually replaced by ApiService (see below), but the shape differs enough to treat as removed + new rather than a rename.
  • InboxFlowInstance, removed; replaced by LogService.
  • getCaller (standalone function), replaced by DebugService.getCaller() (static method).
  • httpDebug, replaced by flushDebugLogs (env var gate also changed: SHOW_ERRORS_IN_CONSOLEDEBUG_MODE).

New exports

  • ApiService, HTTP client class. new ApiService({ baseUrl, baseHeaders?, baseToken?, enableDebug? }), methods get/post/put/patch/delete(endpoint, { headers?, token?, urlParams?, body? }) (lowercase methods, object-based args, new urlParams templating).
  • DebugService, setIgnoreFile(file), clearIgnoreFiles(), getCaller().
  • LogService, setConfig({trafficSourceId, userToken, logBaseApiUrl?}), getConfig(), resetConfig().
  • flushDebugLogs, env-gated colored debug log printer.
  • formAsyncParse, async counterpart to formParse, using safeParseAsync for schemas with async refinements.
  • validateCep, validateCnpj, validateCpf, validateDate, validatePassword, validatePhone, validateRg, moved here from @arkyn/shared (see @arkyn/shared).
  • validateEmail(rawEmail): Promise<boolean>, genuinely new; format + RFC-5322-ish checks plus a live DNS MX/A/AAAA lookup.

Changed exports (breaking behavior)

  • All success-response classes (Created, Found, Updated, Success, NoContent): constructor signature inverted. Old: (body: T, init?: ResponseInit) (status/headers overridable via init). New: (message: string, body?: any) with hard-coded status codes per class and no way to override status/headers. Every call site like new Created(body) must become new Created("message", body).
  • UnprocessableEntity: constructor's second enableDebug boolean parameter is removed, debug logging always fires now (env-gated only).
  • SchemaValidator<T>: generic constraint T extends Schema (zod v3) → T extends ZodType (zod v4); return type SafeParseReturnTypeZodSafeParseResult. New method formAsyncValidate().
  • errorHandler: the fallback 500 now passes the original error as cause (new ServerError("Server error", error)) instead of dropping it.
  • validateDate (moved from shared): config.inputFormat literals renamed from "DD/MM/YYYY"|"MM-DD-YYYY"|"YYYY-MM-DD" to "brazilianDate"|"isoDate"|"timestamp".
  • validatePhone (moved from shared): now expects compact E.164 input ("+5532912345678") via libphonenumber-js, not the old space-separated "+55 32912345678" format.
  • validateCnpj/validateCpf/validatePassword: same signatures, minor hardening (extra length/whitespace guards; validatePassword's special-char regex now also accepts ;).

@arkyn/shared

Package metadata

  • main
    • v2.2.3: ./dist/bundle.js
    • v3.0.2: ./dist/index.js
  • exports map
    • v2.2.3: none
    • v3.0.2: one subpath per function
  • dependencies
    • v2.2.3: uuid ^10.0.0
    • v3.0.2: @arkyn/templates (now correctly declared), uuid dropped
  • peerDependencies
    • v2.2.3: none
    • v3.0.2: libphonenumber-js >=1.13.7 (optional, only if calling formatToPhone/findCountryMask)

Removed exports (moved to @arkyn/server)

validateCep, validateCnpj, validateCpf, validatePassword, validateRg, validateDate (renamed enum literals, see above), validatePhone (rewritten to expect E.164 + libphonenumber-js, see above). formatToCpfCnpj is genuinely deleted with no replacement, call formatToCpf/formatToCnpj directly.

New exports

  • formatToCapitalizeFirstWordLetter(sentence): string
  • findCountryMask(phoneNumber): [string, CountryType], parses E.164 via libphonenumber-js, resolves the @arkyn/templates country/mask.
  • isHtml(rawString): boolean, moved in from @arkyn/components.
  • ValidateDateService (class), low-level date-part/format validation used internally by formatDate/parseToDate.

Renamed exports

  • truncateLargeFieldsparseLargeFields (same signature/behavior).
  • maskSensitiveDataparseSensitiveData (same signature/behavior).
  • formatToDateparseToDate, same signature, but behavior changed: "isoDate" now means MM-DD-YYYY (was YYYY-MM-DD), and the timezone offset sign is flipped (a call with a negative/positive timezone argument returns a different UTC time than before, verify any code passing an explicit timezone).

Changed exports (breaking behavior)

  • formatDate: same signature; "isoDate"/"timestamp" literal meanings swapped as above; now throws descriptive errors for invalid calendar dates (e.g. Feb 30) instead of silently producing NaN; date math switched from local-timezone Date construction to explicit Date.UTC, removing a host-timezone-dependent bug but changing output for machines in non-UTC zones.
  • formatToCep/formatToCnpj/formatToCpf: error message text changed from generic "Invalid ... format" to descriptive "<Type> must be contain N numeric digits: <value>", breaking if you pattern-match on the thrown message.
  • formatToPhone: fully rewritten. Old expected "+55-32 912345678"-style input; new expects plain E.164 ("+5534920524282") via libphonenumber-js + the new findCountryMask. Old-style input will throw.
  • formatToHiddenDigits: options is now optional (was required).
  • formatToEllipsis: truncation now breaks at whitespace boundaries and strips trailing punctuation, output can differ from v2.2.3 for the same input.
  • calculateCardInstallment: rounding order changed (rounds installmentPrice first, then derives totalPrice), can shift the returned total by a cent versus v2.2.3 for some inputs.
  • stripHtmlTags: now also strips <script>/<style> blocks and HTML comments, not just bare tags.
  • generateId: uuid dependency dropped in favor of native crypto.randomUUID()/crypto.getRandomValues(), same overloads/output shape, no uuid transitive dependency anymore.

@arkyn/templates

Package metadata

  • main
    • v2.2.3: ./dist/bundle.js
    • v3.0.2: ./dist/index.js
  • exports map
    • v2.2.3: none
    • v3.0.2: ./brazilianStates, ./countries, ./countryCurrencies, ./maximumFractionDigits subpaths

New exports

  • brazilianStates, new array of 27 {label, value} entries (26 states + Federal District).
  • CountryType, new exported type for countries entries.

Changed exports (breaking data shape)

countries, same 244 entries, but:
  1. The prefix field is removed from every entry (was a nullable string on ~30 NANP +1 territories). Any code reading country.prefix breaks.
  2. Brazil's mask changed from a single string to a two-entry array: ["(__) _____-____", "(__) ____-____"]. Code assuming mask is always a plain string (no Array.isArray check) will break specifically for iso === "BR".

ts

// v2.2.3
{ name: "Brasil", code: "+55", prefix: null, iso: "BR", flag: "...", mask: "(__) _____-____" }
// v3.0.2
{ name: "Brasil", code: "+55", iso: "BR", flag: "...", mask: ["(__) _____-____", "(__) ____-____"] }
countryCurrencies and maximumFractionDigits are unchanged (only indentation-style diffs).

Removed: @arkyn/types

In v2.2.3, shared prop/context types were imported from a standalone @arkyn/types package, e.g.:

ts

import type { AlertContainerProps, ButtonProps, InputProps } from "@arkyn/types";
This package no longer exists in v3.0.2, and there is no direct named-type replacement. Each component's prop type is now declared inline in its own file and, with only a couple of exceptions (FacebookPixelProps, DrawerContextProps), is not re-exported.
Migration action: derive the prop type structurally instead of importing it by name:

ts

import { Button } from "@arkyn/components/button";
type ButtonProps = React.ComponentProps<typeof Button>;

Removed: @arkyn/newcomponents

@arkyn/newcomponents@2.1.0 was an experimental package in v2.2.3 exporting only Badge and Button. It no longer exists as a standalone package, v3.0.2's @arkyn/components Badge/Button are a direct continuation of the @arkyn/newcomponents implementation, not the old stable @arkyn/components one (confirmed by the shared IconRenderer icon-rendering helper, unique to newComponents, versus the old @arkyn/components' GenerateIcon).
  • If you were already using @arkyn/newcomponents: migrating to v3 @arkyn/components is close to a drop-in continuation (same prop shape/variant system), aside from the general @arkyn/types removal and Badge's default-size change ("md""lg") and the new "secondary" scheme.
  • If you were using the old stable @arkyn/components@2.2.3 Badge/Button: treat this as a meaningful internal rewrite (different icon-rendering plumbing) even though the prop surface is largely compatible.

Tooling / packaging changes

  • Module format: every package switches from a single bundled CJS/ESM file (dist/bundle.js, no exports map) to unbundled per-module ESM output (dist/index.js + dist/modules/**) with a granular exports map, enabling subpath imports like @arkyn/components/button or @arkyn/shared/formatToCep instead of only importing from the package root.
  • Publish scope: the root .npmignore is gone; each package now declares an explicit files allowlist in its own package.json (["dist", "README.md", "LICENSE.txt", ...]). README.md/LICENSE.txt are now bundled in every published tarball (they weren't before).
  • Workspaces: root package.json adds apps/* alongside packages/*. apps/development is a new internal Remix/React Router playground app (private: true), not part of the public package surface.
  • Linting: root biome.json (Biome 2.5.1) is new, there was no formal linter/ESLint config in v2.2.3.
  • Engines: v3.0.2 packages declare "engines": { "node": ">=18.0.0", "bun": ">=1.0.0" }; v2.2.3 declared none.

Migration checklist

  1. Uninstall @arkyn/types and @arkyn/newcomponents from your package.json, neither exists in v3.
  2. Install the new peer dependencies your app actually needs: libphonenumber-js (server validators, PhoneInput/CurrencyInput/findCountryMask), mapbox-gl (MapView), zod@^4 (server schema validation, check for v3→v4 breaking changes in your own zod usage), slate/slate-react/slate-history (RichText), lucide-react, react-hot-toast, plus whichever other components you use per the new peerDependenciesMeta table in each package's README.
  3. Import a stylesheet: add import "@arkyn/components/styles" (or per-component .css subpath imports), nothing rendered without a theme/token override before, but the explicit CSS import step is new.
  4. Remove all @remix-run/* coupling assumptions: useFieldErrors, useAutomation, useScopedParams, Pagination, and GoogleProviderPlacesProvider all changed from reading Remix router/action-data hooks internally to taking plain arguments, audit every call site.
  5. Replace the old Form/FormController/FormLabel/FormError pattern with FormProvider + useForm() + FieldWrapper/FieldLabel/FieldError, passing fieldErrors in explicitly.
  6. Fix the isVisibledisVisible typo rename on ModalContainer and DrawerContainer.
  7. Rename onUploadonChange on AudioUpload, FileUpload, ImageUpload; onSelectonChange on Select/MultiSelect; onChangeValueonChange on CurrencyInput; onClickonChange and defaultActivedefaultValue on TabContainer.
  8. Replace any usage of Breadcrumb, Card, Skeleton, CpfCnpjInput, no direct v3 replacement exists; reimplement manually.
  9. Migrate GoogleMapMapView: swap your Google Maps API key for a Mapbox access token, update the coordinates/marker shape, and drop any reliance on draggable markers.
  10. Audit @arkyn/templates countries usage: remove any reads of country.prefix, and guard country.mask against being an array (true today only for Brazil).
  11. Re-check date/phone formatting call sites in @arkyn/shared: formatDate/parseToDate's "isoDate" literal now means MM-DD-YYYY; formatToPhone/validatePhone now expect E.164 input.
  12. Update HTTP response construction in @arkyn/server: new Created(body)new Created("message", body) (and equivalently for Found/Updated/Success/NoContent); remove the second enableDebug argument from UnprocessableEntity.
  13. If you import any prop type by name from @arkyn/components (e.g. SelectProps, SwitchProps, RadioGroupProps), switch to React.ComponentProps<typeof X> since these are no longer exported.
Once you've cleared this checklist, use Breaking Changes and the v3.x.x changelog to track anything that changes going forward, this guide won't be updated for new stable releases.
On this page
    arkyn