arkynChangelogGuides
arkyn
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, currently tagged 3.0.1-beta.207 in package.json pending final release). 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 or the v3.0.1-beta changelog. If you're already on a recent v3.0.1-beta version and just want to know what changed release-to-release, those two 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.1-beta.207) — continued
  • @arkyn/newcomponents (2.1.0, experimental) → removed — merged into @arkyn/components
  • @arkyn/server (2.2.3) → @arkyn/server (3.0.1-beta.207) — continued
  • @arkyn/shared (2.2.3) → @arkyn/shared (3.0.1-beta.207) — continued
  • @arkyn/templates (2.2.3) → @arkyn/templates (3.0.1-beta.207) — 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 propchildren: (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 exportSelectProps 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.
  • useFieldErrorsRemoved. 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.
  • GoogleProviderPlacesProviderBreaking: 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).
  • DebugServicesetIgnoreFile(file), clearIgnoreFiles(), getCaller().
  • LogServicesetConfig({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.0.1-beta changelog to track anything that changes going forward — this guide won't be updated for new beta releases.
On this page