npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@asbirtech/toolkit

v2.6.0

Published

A collection of domain-agnostic utilities, formatters, and React hooks shared across Planout's apps (`web`, `dashboard`, and beyond). Extracted from `@planout/common` to be framework-light and reusable outside this monorepo.

Readme

@asbirtech/toolkit

A collection of domain-agnostic utilities, formatters, and React hooks shared across Planout's apps (web, dashboard, and beyond). Extracted from @planout/common to be framework-light and reusable outside this monorepo.

Install

Inside this workspace it's available via workspace:*:

{
  "dependencies": {
    "@asbirtech/toolkit": "workspace:*"
  }
}

Peer dependencies: react / react-dom (only required if you use @asbirtech/toolkit/hooks).

Import paths

The package exposes a few subpath exports — pick the narrowest one that has what you need:

| Import path | What it contains | |---|---| | @asbirtech/toolkit | Everything from ./utils and ./lib (root barrel). Does not include hooks. | | @asbirtech/toolkit/utils | All utils below, from every file in src/utils. | | @asbirtech/toolkit/utils/url | Just the URL helpers (src/utils/url/url.ts). | | @asbirtech/toolkit/utils/classes | Just mergeClasses. | | @asbirtech/toolkit/lib | normalizeError (Axios/form-error normalization). | | @asbirtech/toolkit/hooks | React hooks (useFormErrors, useScrollIntoView) — requires react. | | @asbirtech/toolkit/* | Generic fallback resolving to any file under dist/*, e.g. @asbirtech/toolkit/utils/url/get-email-from-url for the one helper not re-exported by the ./utils/url barrel. |

import { fCurrency, mergeClasses, slugify } from "@asbirtech/toolkit";
import { useFormErrors } from "@asbirtech/toolkit/hooks";
import { normalizeError } from "@asbirtech/toolkit/lib";

Hooks (@asbirtech/toolkit/hooks)

useFormErrors()

Wraps normalizeError in React state for form submission flows.

const { submissionErrors, handleError, clearErrors } = useFormErrors();

try {
  await api.post("/register", data);
  clearErrors();
} catch (error) {
  handleError(error);
}

// submissionErrors: { message?: string; fields?: Record<string, string | string[]> } | null

useScrollIntoView<T>(options?)

Imperatively scrolls a ref'd element into view, gated by an isReady flag — handy for jumping to the first validation error once a form re-renders.

const { targetRef, requestScroll } = useScrollIntoView<HTMLDivElement>({
  isReady: !isSubmitting,
});

<div ref={targetRef} />
<button onClick={requestScroll}>Jump to error</button>

Options: behavior (default "smooth"), block (default "start"), inline (default "nearest"), isReady (default true).


Lib (@asbirtech/toolkit/lib)

normalizeError(error: unknown): FormSubmissionErrors

Normalizes Axios errors, Error instances, strings, or arbitrary error-shaped objects into a consistent { message, fields } shape for form error display. Pulls field-level validation errors out of error.response.data.errors when present.

try {
  await axios.post("/api/submit", data);
} catch (error) {
  const { message, fields } = normalizeError(error);
}

Utils (@asbirtech/toolkit/utils, also re-exported from the root)

Address — address.ts

normalizeAddress(location) { address?, city?, country? } | string | null | undefined → string | null

Joins non-empty address/city/country fields into a single display string; passes a plain string through unchanged; returns null for empty input.

normalizeAddress({ city: "Manila", country: "PH" }); // "Manila, PH"
normalizeAddress(null); // null

API request logging — api-logging.ts

Building blocks for an in-app API request/error log viewer (e.g. a debug panel that reads .jsonl log files). Every function/constant is exported under two names — a generic Log* name and an Api*-prefixed alias — both point at the same implementation, so use whichever reads better at the call site.

  • API_LOG_FILE_PREFIX / LOG_FILE_PREFIX"api-"
  • API_LOG_FILE_EXTENSION / LOG_FILE_EXTENSION".jsonl"
  • logTimeFilters / apiLogTimeFilters — filter chips: all, last-15m, last-1h, night, morning, afternoon, evening
  • logTypeFilters / apiLogRequestTypeFilters — filter chips: all, GET, POST, PUT, DELETE, ERROR
  • Types: LogEntry/ApiLogEntry, LogErrorInput/ApiErrorLogInput, LogKind/ApiLogKind, LogMethod/ApiLogMethod, LogTimeFilterId/ApiLogTimeFilterId, LogTypeFilterId/ApiLogRequestTypeFilterId
import {
  createLogRequestId,
  createErrorLogEntry,
  serializeLogEntry,
  parseLogJsonl,
  matchesLogTimeFilter,
  matchesLogTypeFilter,
  getLogFileName,
  formatLogFileSize,
} from "@asbirtech/toolkit/utils";

// writing a log line
const entry = createErrorLogEntry(caughtError, { operation: "checkout.submit" });
appendToFile(getLogFileName("2026-08-03"), serializeLogEntry(entry) + "\n");

// reading + filtering in a viewer
const entries = parseLogJsonl(fileContents); // sorted newest-first
const visible = entries.filter(
  (e) => matchesLogTimeFilter(e, "last-1h") && matchesLogTypeFilter(e, "GET"),
);

Other helpers: createLogRequestId(), normalizeLogError(error), parseLogLine(line), safeStringifyLog(value) (handles circular refs/bigints), sortLogEntriesNewestFirst(entries), formatLogDateKey(date), isLogFileName(fileName), dateFromLogFileName(fileName), isLogDateKey(value), formatLogDateLabel(date), formatLogTimeLabel(timestamp), formatLogFileSize(size), getLogStatusLabel(entry), getLogScreenLabel(entry).

Boolean env parsing — boolean.ts

parseBoolEnv(value: string | undefined, defaultValue: boolean): boolean

Parses env-var-style strings ("true"/"1"/"yes"/"on"true, "false"/"0"/"no"/"off"/""false) into a boolean, falling back to defaultValue for anything else (including undefined).

const debugLogging = parseBoolEnv(process.env.DEBUG_LOGGING, false);

Class names — classes/classes.ts

mergeClasses(className?, state?): string

Merges a base class name (string or array of strings) with conditionally-applied state classes. A state value can be a plain boolean (uses the object key as the class name) or a [condition, className] tuple to apply a different class name than the key.

mergeClasses("btn", {
  active: isActive,
  disabled: [!canSubmit, "btn--disabled"],
});
// "btn active btn--disabled" (when isActive && !canSubmit)

Import via @asbirtech/toolkit/utils/classes or the root/utils barrel.

Currency — currency.ts

currencyFormatter — a preconfigured Intl.NumberFormat (en-PH, PHP, 0 fraction digits).

getNumericValue(value?: string | number | null): number — coerces to a number, defaulting to 0 for invalid/empty input.

currencyFormatter.format(1500); // "₱1,500"
getNumericValue("42.5"); // 42.5
getNumericValue(null); // 0

For locale-configurable currency formatting, see fCurrency in format-number.ts below.

Dates — date.ts

Dayjs-based formatting/comparison helpers (extends the duration and relativeTime plugins internally — no setup needed).

  • formatPatterns — reusable dayjs format strings (dateTime, dateTimeWithSeconds, date, time, plus split/paramCase DD/MM variants)
  • today(template?) → today at start of day, formatted
  • fDateTime(date, template?)"Apr 17, 2022 12:00 am" (or "Invalid date")
  • fDateTimeWithSeconds(date, template?)"Sep 15, 2025 05:28:03 PM"
  • fDate(date, template?)"Apr 17, 2022"
  • fTime(date, template?)"12:00 am"
  • formatEventDate(date)"June 27, 2025"
  • formatEventDateWithDay(date)"Friday, June 27, 2025"
  • formatEventTime(date)"4:00 AM GMT+00:00"
  • formatEventDateTime(date){ date, time }
  • formatShortDate(date, format?: "US" | "EU")"04-25-25" or "25-04-25"
  • formatUnixTimestampDate(timestamp?)"Jun 27, 2025" or "—" for null/undefined
  • fTimestamp(date) → Unix seconds, or "Invalid date"
  • fToNow(date) → relative time, e.g. "2 years", "a few seconds"
  • fIsBetween(inputDate, startDate, endDate) / fIsAfter(startDate, endDate) / fIsSame(startDate, endDate, unit?)boolean (fIsSame defaults to comparing by "year")
  • fDateRangeShortLabel(startDate, endDate, initial?) → smart range label
  • fAdd(durationProps) / fSub(durationProps) → ISO string of now ± a duration ({ years, months, days, hours, minutes, seconds, milliseconds }, all optional)
formatEventDateTime(event.startsAt); // { date: "June 27, 2025", time: "4:00 AM GMT+00:00" }
fDateRangeShortLabel(event.startsAt, event.endsAt); // "April 25-26, 2024"
fAdd({ days: 7 }); // one week from now, ISO string
fToNow(comment.createdAt); // "3 hours"

Types: DatePickerFormat, EventDateInput, ShortDateFormat, DurationProps.

Number/currency/percent formatting — format-number.ts

Locale-aware formatters with a settable global locale (defaults to fil-PH / PHP).

setNumberLocale({ code, currency }) — configures the locale used by every formatter below. Call once at app startup if you need a different locale/currency than the default.

  • fNumber(value, options?) → formatted number, "" for invalid input
  • fCurrency(value, options?) → formatted currency string
  • fPercent(value, options?) → formatted percent; input is on a 0–100 scale (50"50%")
  • fShortenNumber(value, options?) → compact notation, lowercased suffix ("1.2k")
  • fData(value) → byte-size string ("1.5 Mb"), "0 bytes" for 0/invalid
  • calculateTotalPercentageAmount(percentage, originalAmount)number
  • formatPhpCurrency(value: number) → fixed en-PH/PHP formatted string, independent of setNumberLocale
  • parseAmount(value?) → strips non-numeric characters and parses to a number, defaulting to 0
setNumberLocale({ code: "en-US", currency: "USD" });
fCurrency(19.99); // "$19.99"
fShortenNumber(15000); // "15k"
fData(1536); // "1.5 Kb"
parseAmount("₱1,250.00"); // 1250

Type: InputNumberValue = string | number | null | undefined.

HTML — html.ts

Browser-only (no-op — returns input unchanged — when document is undefined, e.g. during SSR).

decodeHtmlEntities(text: string): string — decodes entities like &amp; via a throwaway <textarea>.

stripHtmlTags(html: string): string — strips tags, returning the plain text content.

stripHtmlTags("<p>Hello <b>world</b></p>"); // "Hello world"
decodeHtmlEntities("Tom &amp; Jerry"); // "Tom & Jerry"

Images — image.ts

  • SupportedFileFormat["jpeg", "jpg", "png", "webp", "heic", "heif", "pdf"]
  • ImageFileFormat — same, minus "pdf"
  • getAcceptedTypes()".jpeg,.jpg,.png,.webp,.heic,.heif,.pdf" (for <input accept>)
  • getAcceptedImageTypes() → same, image-only (no pdf)
  • getAcceptedImageTypesForInput()"image/jpeg,image/png,..." MIME list

compressAndConvertToJpeg(file: File, quality = 0.8, maxWidthOrHeight = 1920): Promise<File>

Converts HEIC/HEIF to JPEG (via heic2any), then compresses via browser-image-compression. Browser-only — dynamically imports both libraries so importing this module doesn't break SSR; returns the original file unchanged if called on the server.

<input
  type="file"
  accept={getAcceptedImageTypesForInput()}
  onChange={async (e) => {
    const file = e.target.files?.[0];
    if (file) {
      const compressed = await compressAndConvertToJpeg(file);
      uploadFile(compressed);
    }
  }}
/>

Masking — mask.ts

maskEmail(email: string): string"jo***@example.com" (keeps up to 2 leading local-part characters).

maskPhone(phone: string): string → masks all but the last 4 digits, e.g. "+15551234567""***********4567".

maskEmail("[email protected]"); // "jo***@example.com"
maskPhone("+15551234567"); // "***********4567"

Localhost detection — network.ts

isLocalhost: boolean

true when running in a browser on localhost, 127.0.0.1, or a 192.168.x.x address — useful for gating dev-only UI or behavior.

if (isLocalhost) console.log("dev mode");

Numbers — number.ts

toFiniteNumber(value: unknown): number | null

Coerces a value to a positive integer; returns null for 0, negatives, non-integers, or anything that doesn't parse.

parseNonNegativeInteger(value: string | undefined, fallback: number): number

Parses a non-negative integer from a string (e.g. a query param), returning fallback if invalid.

toFiniteNumber("42"); // 42
toFiniteNumber("-3"); // null
parseNonNegativeInteger(searchParams.get("page") ?? undefined, 1); // 1

Query strings — query-string.ts

createQueryString(currentSearchParams, params): string

Merges new key/value pairs into an existing query string (string or URLSearchParams); a null/undefined/"" value in params deletes that key.

createQueryString(window.location.search, { page: 2, filter: null });
// "page=2" — filter removed if it existed

Next.js searchParams helpers — search-params.ts

buildQueryString(searchParams?: SearchParams): string

Turns a Next.js searchParams object (Record<string, string | string[] | undefined>) into a "?a=1&b=2" string, or "" when empty.

buildUrlWithParams(basePath, params): string

Appends a query string to a path, skipping any undefined/null/"" values.

buildQueryString({ category: ["music", "art"], q: "search" }); // "?category=music&category=art&q=search"
buildUrlWithParams("/events", { category: "music", page: undefined }); // "/events?category=music"

Type: SearchParams.

Scrolling — scroll.ts

scrollToId(id: string, options?: ScrollIntoViewOptions): boolean

Finds an element by id and scrolls it into view (default { behavior: "smooth", block: "center" }); returns whether the element existed. Browser-only (false during SSR).

scrollToId("section-2"); // scrolls smoothly, centers the element, returns true/false

For scroll-on-ready-condition flows tied to React state, prefer the useScrollIntoView hook.

Strings — string.ts

slugify(text: string): string — lowercase, hyphenated slug.

sanitizeDashes(source: string): string — replaces dashes/underscores with spaces and trims to the first 3 words.

toReadableLabel(value: string): string — converts camelCase/kebab-case/snake_case into "Title Case" words.

parseBoolean(value?: string): boolean — strictly true only when the trimmed, lowercased string equals "true".

toReadableLabel("firstName"); // "First Name"
toReadableLabel("event-status"); // "Event Status"
slugify("Mountaineering Club!"); // "mountaineering-club"
parseBoolean("True"); // true
parseBoolean("yes"); // false — only "true" counts

Note: slugify (here) and toSlug (below, to-slug.ts) implement very similar slug logic independently — both are exported (under different names, so no collision), but treat them as the same feature when reaching for one.

Slugs — to-slug.ts

toSlug(str: string): string

Converts a string to a URL-friendly slug (lowercase, hyphenated, strips special characters).

toSlug("Mountaineering Club"); // "mountaineering-club"

Uploads — upload.ts

extractUploadedFileId(value: unknown): number | null

Pulls a numeric file id out of varied upload-response shapes — a raw number/string, { file_id }, { id }, or a nested { data: { file_id | id } } — using toFiniteNumber for coercion.

extractUploadedFileId({ data: { file_id: "123" } }); // 123
extractUploadedFileId(null); // null

Environment variables — env.ts

getRequiredEnv(name: string, value: string | undefined): string

Guards a required environment variable at startup, throwing a clear error instead of silently continuing with undefined.

const apiUrl = getRequiredEnv(
  "NEXT_PUBLIC_REST_API_ENDPOINT",
  process.env.NEXT_PUBLIC_REST_API_ENDPOINT,
);

URLs — url/url.ts (also @asbirtech/toolkit/utils/url)

hasParams(url: string): boolean — whether a URL string has any query params (SSR-safe).

removeLastSlash(pathname: string): string — strips one trailing slash (leaves "/" alone).

isEqualPath(targetUrl, currentUrl, options?: { deep?: boolean }): boolean — compares pathnames (and, when deep: true, query params too — the default) after normalizing trailing slashes.

removeParams(url: string): string — returns just the pathname, stripped of the query string. Resolves relative URLs against window.location.origin, so it's effectively client-only for relative input.

isExternalLink(url: string): booleantrue if the URL starts with http:///https://.

buildFullUrl(baseURL, url): string — joins a base URL and a path; if url is already an external link, returns it unchanged.

safeReturnUrl(value: string | null, fallback?: string | null): string — validates that a redirect URL is a safe, same-origin, non-junk internal path before using it (open-redirect protection). Defaults the fallback to "/".

isExternalLink("https://google.com"); // true
buildFullUrl("https://api.planout.io", "/events"); // "https://api.planout.io/events"
safeReturnUrl(router.query.redirect as string, "/dashboard"); // safe internal path, or "/dashboard"
isEqualPath("/dashboard/", "/dashboard"); // true

Type: EqualPathOptions.

Email prefill from URL — url/get-email-from-url.ts

Not re-exported from the ./utils/url or ./utils barrels — import it via the generic subpath: @asbirtech/toolkit/utils/url/get-email-from-url.

getPrefillEmail(searchParams: Pick<URLSearchParams, "get">): string | null

Extracts an email to prefill on an auth/login page, checking either a direct ?email= param or a nested ?redirect=...?email=... param (handles space→+ normalization and URL decoding).

import { getPrefillEmail } from "@asbirtech/toolkit/utils/url/get-email-from-url";

getPrefillEmail(new URLSearchParams(location.search)); // "[email protected]"

Development

pnpm --filter @asbirtech/toolkit build          # tsc build to dist/
pnpm --filter @asbirtech/toolkit dev            # tsc --watch
pnpm --filter @asbirtech/toolkit test           # vitest run
pnpm --filter @asbirtech/toolkit check-types    # tsc --noEmit
pnpm --filter @asbirtech/toolkit lint           # eslint src

The package is consumed via its built dist/ output by other workspace packages — after editing src/, rebuild (or run the dev watcher) before changes are picked up elsewhere in the monorepo.